├── .gitignore ├── .ruby-gemset ├── .ruby-version ├── Dockerfile ├── Dockerfile.worker ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── README.md ├── Rakefile ├── app ├── assets │ ├── images │ │ ├── fonts │ │ │ ├── AtlasGrotesk-Black-Web.eot │ │ │ ├── AtlasGrotesk-Black-Web.svg │ │ │ ├── AtlasGrotesk-Black-Web.ttf │ │ │ ├── AtlasGrotesk-Black-Web.woff │ │ │ ├── AtlasGrotesk-Bold-Web.eot │ │ │ ├── AtlasGrotesk-Bold-Web.svg │ │ │ ├── AtlasGrotesk-Bold-Web.ttf │ │ │ ├── AtlasGrotesk-Bold-Web.woff │ │ │ ├── AtlasGrotesk-Light-Web.eot │ │ │ ├── AtlasGrotesk-Light-Web.svg │ │ │ ├── AtlasGrotesk-Light-Web.ttf │ │ │ ├── AtlasGrotesk-Light-Web.woff │ │ │ ├── AtlasGrotesk-LightItalic-Web.eot │ │ │ ├── AtlasGrotesk-LightItalic-Web.svg │ │ │ ├── AtlasGrotesk-LightItalic-Web.ttf │ │ │ ├── AtlasGrotesk-LightItalic-Web.woff │ │ │ ├── AtlasGrotesk-Regular-Web.eot │ │ │ ├── AtlasGrotesk-Regular-Web.svg │ │ │ ├── AtlasGrotesk-Regular-Web.ttf │ │ │ ├── AtlasGrotesk-Regular-Web.woff │ │ │ ├── AtlasGrotesk-RegularItalic-Web.eot │ │ │ ├── AtlasGrotesk-RegularItalic-Web.svg │ │ │ ├── AtlasGrotesk-RegularItalic-Web.ttf │ │ │ └── AtlasGrotesk-RegularItalic-Web.woff │ │ ├── propublica-logo-light.min.svg │ │ ├── propublica-logo-white.png │ │ └── propublica-logo.png │ ├── javascripts │ │ ├── application.js │ │ ├── congressapi.js │ │ ├── jquery-1.7.1.min.js │ │ ├── jquery.colorbox-min.js │ │ ├── jquery.placehold-0.3.min.js │ │ ├── jquery.tmpl.min.js │ │ ├── opencivicdataapi.js │ │ └── politwoops.js │ └── stylesheets │ │ ├── admin.css │ │ ├── application.css.scss │ │ ├── home.css.scss │ │ ├── layout.scss.erb │ │ ├── propublica_base │ │ ├── README.md │ │ ├── base.css │ │ ├── master.css │ │ ├── print.css │ │ ├── propublica-text.css │ │ ├── reset.css │ │ └── woland.css │ │ └── search.css.scss ├── controllers │ ├── admin │ │ ├── admin_controller.rb │ │ ├── offices_controller.rb │ │ ├── politicians_controller.rb │ │ ├── reports_controller.rb │ │ ├── system_controller.rb │ │ └── tweets_controller.rb │ ├── application_controller.rb │ ├── errors_controller.rb │ ├── parties_controller.rb │ ├── politicians_controller.rb │ └── tweets_controller.rb ├── helpers │ ├── application_helper.rb │ └── tweets_helper.rb ├── models │ ├── account_link.rb │ ├── account_type.rb │ ├── deleted_tweet.rb │ ├── office.rb │ ├── party.rb │ ├── politician.rb │ ├── tweet.rb │ └── tweet_image.rb ├── services │ ├── export │ │ ├── export_fixtures.rb │ │ └── export_tweet_subgraph_fixtures.rb │ ├── failure.rb │ ├── requeue_tweet.rb │ ├── service_base.rb │ └── success.rb └── views │ ├── admin │ ├── offices │ │ ├── add.html.erb │ │ ├── edit.html.erb │ │ └── list.html.erb │ ├── politicians │ │ ├── _identifier_suggestion_templates.html.erb │ │ ├── admin_list.html.erb │ │ ├── admin_show.html.erb │ │ ├── admin_user.html.erb │ │ ├── get_twitter_id.json.erb │ │ └── new_user.html.erb │ ├── reports │ │ └── annual.html.erb │ ├── system │ │ └── status.html.erb │ └── tweets │ │ └── index.html.erb │ ├── errors │ ├── not_found.html.erb │ └── status_5xx.html.erb │ ├── layouts │ ├── _analytics.html.erb │ ├── admin.html.erb │ └── application.html.erb │ ├── politicians │ ├── all.html.erb │ └── show.html.erb │ ├── shared │ ├── filterform.html.erb │ ├── pager.html.erb │ └── screenshotbox.html.erb │ └── tweets │ ├── _tweet.html.erb │ ├── index.html.erb │ ├── index.rss.builder │ └── show.html.erb ├── bin ├── generate_sitemap.sh ├── logrotate.sh ├── reset_avatars.sh ├── run-collector ├── run-collector-dockercmd ├── run-politwoops-worker ├── run-screenshot-worker ├── run-tweets-client ├── twoops-tweets-client.sh └── twoops-workers-start.sh ├── config.ru ├── config ├── admin.yml.example ├── application.rb ├── boot.rb ├── config.yml.example ├── database.yml.example ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── 90_javascript_exports.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── global_settings.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── paperclip.rb │ ├── secret_token.rb │ ├── session_store.rb │ └── twitter.rb ├── locales │ ├── en.yml │ └── nl.yml ├── routes.rb ├── secrets.yml ├── sitemap.rb └── unicorn.conf.rb ├── db ├── migrate │ ├── 20120904211055_add_state_to_politician.rb │ ├── 20120904211225_create_account_type_table.rb │ ├── 20120904211632_create_office_table.rb │ ├── 20120905195444_rename_office_table_to_offices.rb │ ├── 20120905220238_change_column_on_account_type.rb │ ├── 20120906150719_change_columns_on_politicians.rb │ ├── 20120906151117_change_column_on_pol_account.rb │ ├── 20120906181703_create_account_links.rb │ ├── 20120910213133_add_name_fields_to_politician.rb │ ├── 20120914202322_add_field_to_party.rb │ ├── 20121003160601_add_tweets_modified_index.rb │ ├── 20121108214001_add_tweet_image_tweet_id_index.rb │ ├── 20131107162403_add_retweet_fields.rb │ ├── 20131107190109_populate_retweet_fields.rb │ ├── 20131112174453_add_avatar_columns_to_politician.rb │ ├── 20131217182115_add_composite_indexes_for_politican_joins.rb │ ├── 20131219173338_add_composite_index_politician_and_created.rb │ ├── 20140303200539_add_gender_to_politician.rb │ ├── 20140612143804_add_identifiers_to_politician.rb │ └── 20160125211055_change_approval_defaults.rb ├── schema.rb └── seeds.rb ├── lib ├── core_ext.rb ├── core_ext │ └── string.rb ├── javascript_exports.rb └── tasks │ ├── .gitkeep │ ├── cache.rake │ ├── dbfix.rake │ ├── export.rake │ ├── politicians.rake │ └── tweets.rake ├── public ├── 404.html ├── 500.html └── robots.txt ├── script ├── rails └── reverse ├── twoopsters.csv ├── unicorn.rb.example └── vendor └── cache ├── actionmailer-4.2.7.gem ├── actionpack-4.2.7.gem ├── actionpack-action_caching-1.1.1.gem ├── actionview-4.2.7.gem ├── activejob-4.2.7.gem ├── activemodel-4.2.7.gem ├── activerecord-4.2.7.gem ├── activesupport-4.2.7.gem ├── addressable-2.5.0.gem ├── arel-6.0.3.gem ├── awesome_print-1.7.0.gem ├── aws-sdk-2.6.22.gem ├── aws-sdk-core-2.6.22.gem ├── aws-sdk-resources-2.6.22.gem ├── aws-sigv4-1.0.0.gem ├── axiom-types-0.1.1.gem ├── beanstalk-client-1.1.1.gem ├── buftok-0.2.0.gem ├── builder-3.2.2.gem ├── climate_control-0.0.3.gem ├── cocaine-0.5.8.gem ├── coderay-1.1.1.gem ├── coercible-1.0.0.gem ├── comma-3.2.4.gem ├── concurrent-ruby-1.0.2.gem ├── dalli-2.7.6.gem ├── descendants_tracker-0.0.4.gem ├── domain_name-0.5.20161021.gem ├── equalizer-0.0.10.gem ├── erubis-2.7.0.gem ├── faraday-0.9.2.gem ├── gender_detector-1.0.0.gem ├── globalid-0.3.7.gem ├── http-1.0.4.gem ├── http-cookie-1.0.3.gem ├── http-form_data-1.0.1.gem ├── http_parser.rb-0.6.0.gem ├── httparty-0.10.2.gem ├── i18n-0.7.0.gem ├── ice_nine-0.11.2.gem ├── jmespath-1.3.1.gem ├── json-1.8.3.gem ├── kgio-2.10.0.gem ├── loofah-2.0.3.gem ├── mail-2.6.4.gem ├── memcache-1.3.0.gem ├── memcache-client-1.8.5.gem ├── memoizable-0.4.2.gem ├── method_source-0.8.2.gem ├── mime-types-3.1.gem ├── mime-types-data-3.2016.0521.gem ├── mini_portile2-2.1.0.gem ├── minitest-5.9.1.gem ├── multi_json-1.12.1.gem ├── multi_xml-0.5.5.gem ├── multipart-post-2.0.0.gem ├── mysql2-0.3.21.gem ├── naught-1.1.0.gem ├── nokogiri-1.6.8.1.gem ├── oauth-0.5.1.gem ├── paperclip-2.7.0.gem ├── pry-0.9.12.6.gem ├── pry_debug-0.1.0.gem ├── public_suffix-2.0.4.gem ├── rack-1.6.5.gem ├── rack-test-0.6.3.gem ├── rails-4.2.7.gem ├── rails-deprecated_sanitizer-1.0.3.gem ├── rails-dom-testing-1.0.7.gem ├── rails-html-sanitizer-1.0.3.gem ├── rails_autolink-1.1.6.gem ├── railties-4.2.7.gem ├── raindrops-0.17.0.gem ├── rake-11.3.0.gem ├── rmagick-2.16.0.gem ├── sass-3.4.22.gem ├── sass-rails-5.0.6.gem ├── simple_oauth-0.3.1.gem ├── sitemap_generator-4.3.1.gem ├── slop-3.6.0.gem ├── sprockets-3.7.0.gem ├── sprockets-rails-3.2.0.gem ├── thor-0.19.1.gem ├── thread_safe-0.3.5.gem ├── tilt-2.0.5.gem ├── twitter-5.16.0.gem ├── twitter-text-1.14.1.gem ├── tzinfo-1.2.2.gem ├── unf-0.1.4.gem ├── unf_ext-0.0.7.2.gem ├── unicorn-5.2.0.gem ├── virtus-1.0.5.gem └── will_paginate-3.1.5.gem /.gitignore: -------------------------------------------------------------------------------- 1 | public/sitemap.xml 2 | public/sitemap.xml.gz 3 | public/images/avatars/* 4 | /.bundle 5 | /db/*.sqlite3 6 | /log/*.log 7 | /tmp/**/* 8 | /vendor/gems 9 | /*.csv 10 | */.DS_Store 11 | /unicorn.rb 12 | /config/admin.yml 13 | /config/config.yml 14 | /config/database.yml 15 | .*.swp 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | politwoops 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.2.3 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.2 2 | 3 | RUN apt-get update --fix-missing 4 | 5 | # application dependencies 6 | RUN apt-get install -y libmysqlclient-dev libpq-dev libcurl4-openssl-dev nodejs 7 | RUN echo "gem: --no-ri --no-rdoc" > ~/.gemrc 8 | 9 | WORKDIR /web/ 10 | ADD Gemfile /web/ 11 | ADD Gemfile.lock /web/ 12 | ADD ./vendor/cache /web/vendor/cache 13 | RUN bundle install --deployment --without development --jobs=2 14 | 15 | ADD . /web/ 16 | 17 | ENV RAILS_ENV production 18 | 19 | # if you need to run post-deploy rake tasks that bake or precompute something 20 | # on the local filesystem, do it here 21 | #CMD bundle exec rake do_the_post_deploy_things 22 | 23 | EXPOSE 80 24 | CMD bundle exec unicorn -c ./config/unicorn.conf.rb 25 | -------------------------------------------------------------------------------- /Dockerfile.worker: -------------------------------------------------------------------------------- 1 | FROM ruby:2.2 2 | 3 | RUN apt-get update --fix-missing 4 | 5 | # application dependencies 6 | RUN apt-get install -y libmysqlclient-dev libpq-dev libcurl4-openssl-dev nodejs 7 | RUN echo "gem: --no-ri --no-rdoc" > ~/.gemrc 8 | 9 | WORKDIR /web/ 10 | ADD Gemfile /web/ 11 | ADD Gemfile.lock /web/ 12 | ADD ./vendor/cache /web/vendor/cache 13 | RUN bundle install --deployment --without development --jobs=2 14 | 15 | ADD . /web/ 16 | 17 | ENV RAILS_ENV production 18 | 19 | ENV PHANTOMJS_VERSION 2.1.1 20 | 21 | # Commands 22 | RUN \ 23 | apt-get update && \ 24 | apt-get upgrade -y && \ 25 | apt-get install -y vim git wget libfreetype6 libfontconfig bzip2 && \ 26 | mkdir -p /srv/var && \ 27 | wget -q --no-check-certificate -O /tmp/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 && \ 28 | tar -xjf /tmp/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -C /tmp && \ 29 | rm -f /tmp/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 && \ 30 | mv /tmp/phantomjs-$PHANTOMJS_VERSION-linux-x86_64/ /srv/var/phantomjs && \ 31 | ln -s /srv/var/phantomjs/bin/phantomjs /usr/bin/phantomjs && \ 32 | git clone https://github.com/n1k0/casperjs.git /srv/var/casperjs && \ 33 | ln -s /srv/var/casperjs/bin/casperjs /usr/bin/casperjs 34 | 35 | RUN apt-get install -y beanstalkd 36 | 37 | # Clean up APT when done. 38 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 39 | 40 | VOLUME ["/data"] 41 | CMD bin/run-collector-dockercmd 42 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '4.2.7' 4 | gem "actionpack-action_caching" 5 | gem 'mysql2', '~> 0.3.18' 6 | gem 'memcache', '1.3.0' 7 | gem 'memcache-client' 8 | gem 'virtus', '~> 1.0.2' 9 | 10 | gem 'httparty', '~> 0.10.0' # used for syncing Twitter avatars 11 | gem 'sitemap_generator', '~> 4.0' 12 | 13 | # interacting with Twitter 14 | gem "twitter" 15 | gem "oauth" 16 | 17 | gem 'twitter-text' # parsing hashtags and usernames 18 | gem "comma", "~> 3.0" 19 | gem "gender_detector" 20 | 21 | gem "will_paginate", "~> 3.0.pre2" # pagination 22 | gem "rails_autolink" # auto_link function 23 | 24 | #gem "system_timer", "~> 1.2.4" 25 | gem "beanstalk-client" 26 | 27 | gem "rmagick", "~> 2.0" 28 | gem "paperclip", "2.7.0" 29 | gem "aws-sdk" 30 | gem 'sass-rails' 31 | gem 'unicorn' 32 | gem 'dalli' 33 | 34 | # allows the app to be run with "bundle exec unicorn" in development 35 | group :development do 36 | gem 'awesome_print' 37 | gem 'pry' 38 | gem 'pry_debug' 39 | gem 'unicorn' 40 | # gem 'debugger' 41 | end 42 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.2.7) 5 | actionpack (= 4.2.7) 6 | actionview (= 4.2.7) 7 | activejob (= 4.2.7) 8 | mail (~> 2.5, >= 2.5.4) 9 | rails-dom-testing (~> 1.0, >= 1.0.5) 10 | actionpack (4.2.7) 11 | actionview (= 4.2.7) 12 | activesupport (= 4.2.7) 13 | rack (~> 1.6) 14 | rack-test (~> 0.6.2) 15 | rails-dom-testing (~> 1.0, >= 1.0.5) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 17 | actionpack-action_caching (1.1.1) 18 | actionpack (>= 4.0.0, < 5.0) 19 | actionview (4.2.7) 20 | activesupport (= 4.2.7) 21 | builder (~> 3.1) 22 | erubis (~> 2.7.0) 23 | rails-dom-testing (~> 1.0, >= 1.0.5) 24 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 25 | activejob (4.2.7) 26 | activesupport (= 4.2.7) 27 | globalid (>= 0.3.0) 28 | activemodel (4.2.7) 29 | activesupport (= 4.2.7) 30 | builder (~> 3.1) 31 | activerecord (4.2.7) 32 | activemodel (= 4.2.7) 33 | activesupport (= 4.2.7) 34 | arel (~> 6.0) 35 | activesupport (4.2.7) 36 | i18n (~> 0.7) 37 | json (~> 1.7, >= 1.7.7) 38 | minitest (~> 5.1) 39 | thread_safe (~> 0.3, >= 0.3.4) 40 | tzinfo (~> 1.1) 41 | addressable (2.5.0) 42 | public_suffix (~> 2.0, >= 2.0.2) 43 | arel (6.0.3) 44 | awesome_print (1.7.0) 45 | aws-sdk (2.6.22) 46 | aws-sdk-resources (= 2.6.22) 47 | aws-sdk-core (2.6.22) 48 | aws-sigv4 (~> 1.0) 49 | jmespath (~> 1.0) 50 | aws-sdk-resources (2.6.22) 51 | aws-sdk-core (= 2.6.22) 52 | aws-sigv4 (1.0.0) 53 | axiom-types (0.1.1) 54 | descendants_tracker (~> 0.0.4) 55 | ice_nine (~> 0.11.0) 56 | thread_safe (~> 0.3, >= 0.3.1) 57 | beanstalk-client (1.1.1) 58 | buftok (0.2.0) 59 | builder (3.2.2) 60 | climate_control (0.0.3) 61 | activesupport (>= 3.0) 62 | cocaine (0.5.8) 63 | climate_control (>= 0.0.3, < 1.0) 64 | coderay (1.1.1) 65 | coercible (1.0.0) 66 | descendants_tracker (~> 0.0.1) 67 | comma (3.2.4) 68 | activesupport (>= 3.0.0) 69 | concurrent-ruby (1.0.2) 70 | dalli (2.7.6) 71 | descendants_tracker (0.0.4) 72 | thread_safe (~> 0.3, >= 0.3.1) 73 | domain_name (0.5.20161021) 74 | unf (>= 0.0.5, < 1.0.0) 75 | equalizer (0.0.10) 76 | erubis (2.7.0) 77 | faraday (0.9.2) 78 | multipart-post (>= 1.2, < 3) 79 | gender_detector (1.0.0) 80 | globalid (0.3.7) 81 | activesupport (>= 4.1.0) 82 | http (1.0.4) 83 | addressable (~> 2.3) 84 | http-cookie (~> 1.0) 85 | http-form_data (~> 1.0.1) 86 | http_parser.rb (~> 0.6.0) 87 | http-cookie (1.0.3) 88 | domain_name (~> 0.5) 89 | http-form_data (1.0.1) 90 | http_parser.rb (0.6.0) 91 | httparty (0.10.2) 92 | multi_json (~> 1.0) 93 | multi_xml (>= 0.5.2) 94 | i18n (0.7.0) 95 | ice_nine (0.11.2) 96 | jmespath (1.3.1) 97 | json (1.8.3) 98 | kgio (2.10.0) 99 | loofah (2.0.3) 100 | nokogiri (>= 1.5.9) 101 | mail (2.6.4) 102 | mime-types (>= 1.16, < 4) 103 | memcache (1.3.0) 104 | memcache-client (1.8.5) 105 | memoizable (0.4.2) 106 | thread_safe (~> 0.3, >= 0.3.1) 107 | method_source (0.8.2) 108 | mime-types (3.1) 109 | mime-types-data (~> 3.2015) 110 | mime-types-data (3.2016.0521) 111 | mini_portile2 (2.1.0) 112 | minitest (5.9.1) 113 | multi_json (1.12.1) 114 | multi_xml (0.5.5) 115 | multipart-post (2.0.0) 116 | mysql2 (0.3.21) 117 | naught (1.1.0) 118 | nokogiri (1.6.8.1) 119 | mini_portile2 (~> 2.1.0) 120 | oauth (0.5.1) 121 | paperclip (2.7.0) 122 | activerecord (>= 2.3.0) 123 | activesupport (>= 2.3.2) 124 | cocaine (>= 0.0.2) 125 | mime-types 126 | pry (0.9.12.6) 127 | coderay (~> 1.0) 128 | method_source (~> 0.8) 129 | slop (~> 3.4) 130 | pry_debug (0.1.0) 131 | pry (~> 0.9.0) 132 | public_suffix (2.0.4) 133 | rack (1.6.5) 134 | rack-test (0.6.3) 135 | rack (>= 1.0) 136 | rails (4.2.7) 137 | actionmailer (= 4.2.7) 138 | actionpack (= 4.2.7) 139 | actionview (= 4.2.7) 140 | activejob (= 4.2.7) 141 | activemodel (= 4.2.7) 142 | activerecord (= 4.2.7) 143 | activesupport (= 4.2.7) 144 | bundler (>= 1.3.0, < 2.0) 145 | railties (= 4.2.7) 146 | sprockets-rails 147 | rails-deprecated_sanitizer (1.0.3) 148 | activesupport (>= 4.2.0.alpha) 149 | rails-dom-testing (1.0.7) 150 | activesupport (>= 4.2.0.beta, < 5.0) 151 | nokogiri (~> 1.6.0) 152 | rails-deprecated_sanitizer (>= 1.0.1) 153 | rails-html-sanitizer (1.0.3) 154 | loofah (~> 2.0) 155 | rails_autolink (1.1.6) 156 | rails (> 3.1) 157 | railties (4.2.7) 158 | actionpack (= 4.2.7) 159 | activesupport (= 4.2.7) 160 | rake (>= 0.8.7) 161 | thor (>= 0.18.1, < 2.0) 162 | raindrops (0.17.0) 163 | rake (11.3.0) 164 | rmagick (2.16.0) 165 | sass (3.4.22) 166 | sass-rails (5.0.6) 167 | railties (>= 4.0.0, < 6) 168 | sass (~> 3.1) 169 | sprockets (>= 2.8, < 4.0) 170 | sprockets-rails (>= 2.0, < 4.0) 171 | tilt (>= 1.1, < 3) 172 | simple_oauth (0.3.1) 173 | sitemap_generator (4.3.1) 174 | builder 175 | slop (3.6.0) 176 | sprockets (3.7.0) 177 | concurrent-ruby (~> 1.0) 178 | rack (> 1, < 3) 179 | sprockets-rails (3.2.0) 180 | actionpack (>= 4.0) 181 | activesupport (>= 4.0) 182 | sprockets (>= 3.0.0) 183 | thor (0.19.1) 184 | thread_safe (0.3.5) 185 | tilt (2.0.5) 186 | twitter (5.16.0) 187 | addressable (~> 2.3) 188 | buftok (~> 0.2.0) 189 | equalizer (= 0.0.10) 190 | faraday (~> 0.9.0) 191 | http (~> 1.0) 192 | http_parser.rb (~> 0.6.0) 193 | json (~> 1.8) 194 | memoizable (~> 0.4.0) 195 | naught (~> 1.0) 196 | simple_oauth (~> 0.3.0) 197 | twitter-text (1.14.1) 198 | unf (~> 0.1.0) 199 | tzinfo (1.2.2) 200 | thread_safe (~> 0.1) 201 | unf (0.1.4) 202 | unf_ext 203 | unf_ext (0.0.7.2) 204 | unicorn (5.2.0) 205 | kgio (~> 2.6) 206 | raindrops (~> 0.7) 207 | virtus (1.0.5) 208 | axiom-types (~> 0.1) 209 | coercible (~> 1.0) 210 | descendants_tracker (~> 0.0, >= 0.0.3) 211 | equalizer (~> 0.0, >= 0.0.9) 212 | will_paginate (3.1.5) 213 | 214 | PLATFORMS 215 | ruby 216 | 217 | DEPENDENCIES 218 | actionpack-action_caching 219 | awesome_print 220 | aws-sdk 221 | beanstalk-client 222 | comma (~> 3.0) 223 | dalli 224 | gender_detector 225 | httparty (~> 0.10.0) 226 | memcache (= 1.3.0) 227 | memcache-client 228 | mysql2 (~> 0.3.18) 229 | oauth 230 | paperclip (= 2.7.0) 231 | pry 232 | pry_debug 233 | rails (= 4.2.7) 234 | rails_autolink 235 | rmagick (~> 2.0) 236 | sass-rails 237 | sitemap_generator (~> 4.0) 238 | twitter 239 | twitter-text 240 | unicorn 241 | virtus (~> 1.0.2) 242 | will_paginate (~> 3.0.pre2) 243 | 244 | BUNDLED WITH 245 | 1.12.5 246 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c), Open State Foundation and Sunlight Foundation 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | * Neither the name of the Open State Foundation, Sunlight Foundation nor the 14 | names of its contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNERS OR CONTRIBUTORS BE LIABLE FOR ANY 21 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Setting up the app 2 | ================== 3 | 4 | These instructions assume that you are using RVM, rbenv, or something else that is compatible with .ruby-version. 5 | 6 | * `gem install bundler` 7 | * `bundle install` 8 | * Create `config/database.yml` (see `config/database.yml.example`) 9 | * Create `config/config.yml` (see `config/config.yml.example`) 10 | * `rake db:create` 11 | * `rake db:schema:load` 12 | * `rake db:migrate` 13 | 14 | You can now run rails server and navigate to `/admin/users/` to manually add politicians. Alternatively, you can load politicians in bulk from a CSV spreadsheet using the `rake politicians:import_csv CSV=myfile.csv` command. See twoopsters.csv as an example of the format to use. If you use the `politicians:import_csv` task you should then use the `politicians:reset_avatars` task as well. This will download the avatars currently in use by each politician. 15 | 16 | To restart workers: 17 | 18 | `sudo initctl start beanstalkd` 19 | `sudo initctl restart pt-tweets-client` 20 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | require 'rake/dsl_definition' 7 | require 'rake' 8 | 9 | Politwoops::Application.load_tasks 10 | -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-Black-Web.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-Black-Web.eot -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-Black-Web.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-Black-Web.ttf -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-Black-Web.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-Black-Web.woff -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-Bold-Web.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-Bold-Web.eot -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-Bold-Web.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-Bold-Web.ttf -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-Bold-Web.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-Bold-Web.woff -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-Light-Web.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-Light-Web.eot -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-Light-Web.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-Light-Web.ttf -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-Light-Web.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-Light-Web.woff -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-LightItalic-Web.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-LightItalic-Web.eot -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-LightItalic-Web.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-LightItalic-Web.ttf -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-LightItalic-Web.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-LightItalic-Web.woff -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-Regular-Web.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-Regular-Web.eot -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-Regular-Web.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-Regular-Web.ttf -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-Regular-Web.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-Regular-Web.woff -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-RegularItalic-Web.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-RegularItalic-Web.eot -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-RegularItalic-Web.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-RegularItalic-Web.ttf -------------------------------------------------------------------------------- /app/assets/images/fonts/AtlasGrotesk-RegularItalic-Web.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/fonts/AtlasGrotesk-RegularItalic-Web.woff -------------------------------------------------------------------------------- /app/assets/images/propublica-logo-light.min.svg: -------------------------------------------------------------------------------- 1 | propublica-logoCreated with Sketch. -------------------------------------------------------------------------------- /app/assets/images/propublica-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/propublica-logo-white.png -------------------------------------------------------------------------------- /app/assets/images/propublica-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/app/assets/images/propublica-logo.png -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // the compiled file. 9 | // 10 | // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD 11 | // GO AFTER THE REQUIRES BELOW. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /app/assets/javascripts/congressapi.js: -------------------------------------------------------------------------------- 1 | var CongressApi = (function($){ 2 | function CongressApi (api_key) { 3 | if (this === undefined ) { return new CongressApi(api_key); } 4 | 5 | this.api_key = api_key; 6 | this.url_stub = 'https://congress.api.sunlightfoundation.com'; 7 | }; 8 | 9 | CongressApi.prototype.legislators = function (criteria) { 10 | var $def = new $.Deferred(); 11 | var qparams = $.extend(true, {}, criteria); 12 | qparams['apikey'] = this.api_key; 13 | var leg_request = $.ajax(this.url_stub + '/legislators', { 14 | 'data': qparams 15 | }).done(function(response){ 16 | if ((response.count == null) || (response.count === 0)) { 17 | $def.rejectWith(this, [response]); 18 | } else { 19 | $def.resolveWith(this, [response]); 20 | } 21 | }).fail(function(){ 22 | $def.rejectWith(this, [response]); 23 | }); 24 | return $def; 25 | }; 26 | 27 | return CongressApi; 28 | })(jQuery); 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/assets/javascripts/jquery.placehold-0.3.min.js: -------------------------------------------------------------------------------- 1 | /************************************************************************************ 2 | ** jQuery Placehold version 0.3 3 | ** (cc) Jason Garber (http://sixtwothree.org and http://www.viget.com) 4 | ** Licensed under the CC-GNU GPL (http://creativecommons.org/licenses/GPL/2.0/) 5 | *************************************************************************************/ 6 | 7 | (function(A){A.fn.placehold=function(D){var D=D||"placeholder",C=A.fn.placehold.is_supported();function B(){for(i=0;i",{"class":F.attr("class")+" "+D,value:E});G.bind("focus.placehold",function(){B(F,G);F.focus();});F.bind("blur.placehold",function(){if(!F.val()){B(F,G);}});F.hide().after(G);}F.bind({"focus.placehold":function(){if(F.val()==E){F.removeClass(D).val("");}},"blur.placehold":function(){if(!F.val()){F.addClass(D).val(E);}}});F.closest("form").bind("submit.placehold",function(){if(F.val()==E){F.val("");}return true;});}});};A.fn.placehold.is_supported=function(){return"placeholder" in document.createElement("input");};})(jQuery); -------------------------------------------------------------------------------- /app/assets/javascripts/jquery.tmpl.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Templates Plugin 1.0.0pre 3 | * http://github.com/jquery/jquery-tmpl 4 | * Requires jQuery 1.4.2 5 | * 6 | * Copyright 2011, Software Freedom Conservancy, Inc. 7 | * Dual licensed under the MIT or GPL Version 2 licenses. 8 | * http://jquery.org/license 9 | */ 10 | (function(a){var r=a.fn.domManip,d="_tmplitem",q=/^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,b={},f={},e,p={key:0,data:{}},i=0,c=0,l=[];function g(g,d,h,e){var c={data:e||(e===0||e===false)?e:d?d.data:{},_wrap:d?d._wrap:null,tmpl:null,parent:d||null,nodes:[],calls:u,nest:w,wrap:x,html:v,update:t};g&&a.extend(c,g,{nodes:[],parent:d});if(h){c.tmpl=h;c._ctnt=c._ctnt||c.tmpl(a,c);c.key=++i;(l.length?f:b)[i]=c}return c}a.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(f,d){a.fn[f]=function(n){var g=[],i=a(n),k,h,m,l,j=this.length===1&&this[0].parentNode;e=b||{};if(j&&j.nodeType===11&&j.childNodes.length===1&&i.length===1){i[d](this[0]);g=this}else{for(h=0,m=i.length;h0?this.clone(true):this).get();a(i[h])[d](k);g=g.concat(k)}c=0;g=this.pushStack(g,f,i.selector)}l=e;e=null;a.tmpl.complete(l);return g}});a.fn.extend({tmpl:function(d,c,b){return a.tmpl(this[0],d,c,b)},tmplItem:function(){return a.tmplItem(this[0])},template:function(b){return a.template(b,this[0])},domManip:function(d,m,k){if(d[0]&&a.isArray(d[0])){var g=a.makeArray(arguments),h=d[0],j=h.length,i=0,f;while(i").join(">").split('"').join(""").split("'").join("'")}});a.extend(a.tmpl,{tag:{tmpl:{_default:{$2:"null"},open:"if($notnull_1){__=__.concat($item.nest($1,$2));}"},wrap:{_default:{$2:"null"},open:"$item.calls(__,$1,$2);__=[];",close:"call=$item.calls();__=call._.concat($item.wrap(call,__));"},each:{_default:{$2:"$index, $value"},open:"if($notnull_1){$.each($1a,function($2){with(this){",close:"}});}"},"if":{open:"if(($notnull_1) && $1a){",close:"}"},"else":{_default:{$1:"true"},open:"}else if(($notnull_1) && $1a){"},html:{open:"if($notnull_1){__.push($1a);}"},"=":{_default:{$1:"$data"},open:"if($notnull_1){__.push($.encode($1a));}"},"!":{open:""}},complete:function(){b={}},afterManip:function(f,b,d){var e=b.nodeType===11?a.makeArray(b.childNodes):b.nodeType===1?[b]:[];d.call(f,b);m(e);c++}});function j(e,g,f){var b,c=f?a.map(f,function(a){return typeof a==="string"?e.key?a.replace(/(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g,"$1 "+d+'="'+e.key+'" $2'):a:j(a,e,a._ctnt)}):e;if(g)return c;c=c.join("");c.replace(/^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/,function(f,c,e,d){b=a(e).get();m(b);if(c)b=k(c).concat(b);if(d)b=b.concat(k(d))});return b?b:k(c)}function k(c){var b=document.createElement("div");b.innerHTML=c;return a.makeArray(b.childNodes)}function o(b){return new Function("jQuery","$item","var $=jQuery,call,__=[],$data=$item.data;with($data){__.push('"+a.trim(b).replace(/([\\'])/g,"\\$1").replace(/[\r\t\n]/g," ").replace(/\$\{([^\}]*)\}/g,"{{= $1}}").replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,function(m,l,k,g,b,c,d){var j=a.tmpl.tag[k],i,e,f;if(!j)throw"Unknown template tag: "+k;i=j._default||[];if(c&&!/\w$/.test(b)){b+=c;c=""}if(b){b=h(b);d=d?","+h(d)+")":c?")":"";e=c?b.indexOf(".")>-1?b+h(c):"("+b+").call($item"+d:b;f=c?e:"(typeof("+b+")==='function'?("+b+").call($item):("+b+"))"}else f=e=i.$1||"null";g=h(g);return"');"+j[l?"close":"open"].split("$notnull_1").join(b?"typeof("+b+")!=='undefined' && ("+b+")!=null":"true").split("$1a").join(f).split("$1").join(e).split("$2").join(g||i.$2||"")+"__.push('"})+"');}return __;")}function n(c,b){c._wrap=j(c,true,a.isArray(b)?b:[q.test(b)?b:a(b).html()]).join("")}function h(a){return a?a.replace(/\\'/g,"'").replace(/\\\\/g,"\\"):null}function s(b){var a=document.createElement("div");a.appendChild(b.cloneNode(true));return a.innerHTML}function m(o){var n="_"+c,k,j,l={},e,p,h;for(e=0,p=o.length;e=0;h--)m(j[h]);m(k)}function m(j){var p,h=j,k,e,m;if(m=j.getAttribute(d)){while(h.parentNode&&(h=h.parentNode).nodeType===1&&!(p=h.getAttribute(d)));if(p!==m){h=h.parentNode?h.nodeType===11?0:h.getAttribute(d)||0:0;if(!(e=b[m])){e=f[m];e=g(e,b[h]||f[h]);e.key=++i;b[i]=e}c&&o(m)}j.removeAttribute(d)}else if(c&&(e=a.data(j,"tmplItem"))){o(e.key);b[e.key]=e;h=a.data(j.parentNode,"tmplItem");h=h?h.key:0}if(e){k=e;while(k&&k.key!=h){k.nodes.push(j);k=k.parent}delete e._ctnt;delete e._wrap;a.data(j,"tmplItem",e)}function o(a){a=a+n;e=l[a]=l[a]||g(e,b[e.parent.key+n]||e.parent)}}}function u(a,d,c,b){if(!a)return l.pop();l.push({_:a,tmpl:d,item:this,data:c,options:b})}function w(d,c,b){return a.tmpl(a.template(d),c,b,this)}function x(b,d){var c=b.options||{};c.wrapped=d;return a.tmpl(a.template(b.tmpl),b.data,c,b.item)}function v(d,c){var b=this._wrap;return a.map(a(a.isArray(b)?b.join(""):b).filter(d||"*"),function(a){return c?a.innerText||a.textContent:a.outerHTML||s(a)})}function t(){var b=this.nodes;a.tmpl(null,null,null,this).insertBefore(b[0]);a(b).remove()}})(jQuery); -------------------------------------------------------------------------------- /app/assets/javascripts/opencivicdataapi.js: -------------------------------------------------------------------------------- 1 | var OpenCivicDataApi = (function($){ 2 | function OpenCivicDataApi (api_key) { 3 | if (this === undefined ) { return new OpenCivicDataApi(api_key); } 4 | 5 | this.api_key = api_key; 6 | this.url_stub = 'https://api.opencivicdata.org'; 7 | }; 8 | 9 | OpenCivicDataApi.prototype.people = function (criteria) { 10 | var $def = new $.Deferred(); 11 | var qparams = $.extend(true, {}, criteria); 12 | qparams['apikey'] = this.api_key; 13 | $.ajax(this.url_stub + '/people/', { 14 | 'dataType': 'jsonp', 15 | 'data': qparams 16 | }).done(function(response){ 17 | if ((response.meta == null) || (response.meta.count == null) || (response.meta.count === 0)) { 18 | $def.rejectWith(this, [response]); 19 | } else { 20 | $def.resolveWith(this, [response]); 21 | } 22 | }).fail(function(response){ 23 | $def.rejectWith(this, [response]); 24 | }); 25 | return $def; 26 | }; 27 | 28 | return OpenCivicDataApi; 29 | })(jQuery); 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/assets/javascripts/politwoops.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | var congress_api = new CongressApi(Politwoops.sunlight_api_key); 3 | var ocd_api = new OpenCivicDataApi(Politwoops.sunlight_api_key); 4 | 5 | var swap_attributes = function (elem, attr_a, attr_b) { 6 | var $e = $(elem); 7 | var value_a = $e.attr(attr_a); 8 | var value_b = $e.attr(attr_b); 9 | if ((value_a != null) && (value_b != null)) { 10 | $e.attr(attr_a, value_b); 11 | $e.attr(attr_b, value_a); 12 | } 13 | }; 14 | 15 | $(function(){ 16 | $('img.politician-avatar').error(function(e){ 17 | e.preventDefault(); 18 | swap_attributes(this, 'src', 'data-lateimg-src'); 19 | }); 20 | }); 21 | 22 | $(document).ready(function(){ 23 | $('img.politician-avatar').each(function(ix, img){ 24 | if ($(img).attr('src') !== '') { 25 | swap_attributes(img, 'src', 'data-lateimg-src'); 26 | } 27 | }); 28 | }); 29 | 30 | 31 | 32 | (function($form){ 33 | function extract_name_from_form () { 34 | var last_name = $form.find("input#ln_input").val() || $form.find("input#last_name_input").val(); 35 | var first_name = $form.find("input#fn_input").val() || $form.find("input#first_name_input").val(); 36 | 37 | if ((last_name == null) || (last_name === '')) { 38 | last_name = $form.find("span#last_name").text(); 39 | } 40 | if ((first_name == null) || (first_name === '')) { 41 | first_name = $form.find("span#first_name").text(); 42 | } 43 | return {'first_name': first_name, 'last_name': last_name}; 44 | }; 45 | 46 | function copy_bioguide_identifier_to_form (click) { 47 | var bioguide_id = $(this).text(); 48 | $form.find("input.bioguide_id").val(bioguide_id); 49 | }; 50 | 51 | function copy_ocd_identifier_to_form (click) { 52 | var ocd_id = $(this).text(); 53 | $form.find("input.opencivicdata_id").val(ocd_id); 54 | }; 55 | 56 | var down_arrow = "▼"; 57 | var up_arrow = "▲"; 58 | 59 | $form.find("li.bioguide_id button.expander").toggle( 60 | function showBioguideSuggestions (click) { 61 | var name = extract_name_from_form(); 62 | var $expanded_area = $form.find("li.bioguide_id div.expandable"); 63 | var $leglist = $expanded_area.find("ul.suggestions"); 64 | $leglist.hide().empty(); 65 | $expanded_area.show(); 66 | var name = extract_name_from_form(); 67 | if ((name.last_name == null) || (name.last_name === '')) { 68 | return; 69 | } 70 | congress_api.legislators(name).done(function(response){ 71 | var legislators = response.results; 72 | $("script#bioguide-id-suggestion").tmpl(legislators).appendTo($leglist); 73 | $leglist.find("a.identifier").click(copy_bioguide_identifier_to_form); 74 | $leglist.fadeIn(400); 75 | $form.find("li.bioguide_id button.expander").html(up_arrow); 76 | }); 77 | }, 78 | 79 | function hideBioguideSuggestions () { 80 | $form.find("li.bioguide_id div.expandable").fadeOut(150); 81 | $form.find("li.bioguide_id button.expander").html(down_arrow); 82 | } 83 | ); 84 | 85 | $form.find("li.opencivicdata_id button.expander").toggle( 86 | function showOpenCivicDataSuggestions () { 87 | var $expanded_area = $form.find("li.opencivicdata_id div.expandable"); 88 | var $sugglist = $expanded_area.find("ul.suggestions"); 89 | $sugglist.hide().empty(); 90 | $expanded_area.show(); 91 | var name = extract_name_from_form(); 92 | if ((name.last_name == null) || (name.last_name === '')) { 93 | return; 94 | } 95 | ocd_api.people({'name': name.last_name}).done(function(response){ 96 | var ppl = response.results; 97 | var ppl1 = response.results.filter(function(p){ 98 | return p.name.startsWith(name.first_name); 99 | }); 100 | if (ppl1.length > 0) { 101 | ppl = ppl1; 102 | } 103 | $("script#ocd-id-suggestion").tmpl(ppl).appendTo($sugglist); 104 | $sugglist.find("a.identifier").click(copy_ocd_identifier_to_form); 105 | $sugglist.fadeIn(400); 106 | $form.find("li.opencivicdata_id button.expander").html(up_arrow); 107 | }); 108 | }, 109 | function hideOpenCividDataSuggestions () { 110 | $form.find("li.opencivicdata_id div.expandable").fadeOut(150); 111 | $form.find("li.opencivicdata_id button.expander").html(down_arrow); 112 | $form.find("li.opencivicdata_id ul.suggestions").empty(); 113 | } 114 | ); 115 | 116 | $form.find("input.bioguide_id").change(function(){ 117 | var ocd_id = $form.find(".opencivicdata_id").val(); 118 | if ((ocd_id != null) && (ocd_id !== '')) { 119 | console.log("Going to find OCD id for", $(this).val()); 120 | } 121 | }); 122 | })($("form#admin-politician")); 123 | 124 | 125 | })(jQuery); 126 | 127 | -------------------------------------------------------------------------------- /app/assets/stylesheets/admin.css: -------------------------------------------------------------------------------- 1 | /** Additional admin styles */ 2 | 3 | .admin_header { 4 | position: relative; 5 | height: 70px; 6 | margin-top: -20px; 7 | } 8 | 9 | .admin_header ul.nav { 10 | margin-bottom: 30px; 11 | position: absolute; left: 0; 12 | } 13 | 14 | .admin_header ul.nav li { 15 | display: inline-block; 16 | margin-right: 20px; 17 | font-size: 125%; 18 | } 19 | .admin_header ul.nav li a {text-decoration: underline;} 20 | 21 | .admin_header div.details { 22 | margin-bottom: 10px; 23 | position: absolute; right: 0; 24 | font-size: 90%; 25 | } 26 | 27 | div.admin.review { 28 | margin: 10px 0 10px 0; 29 | } 30 | 31 | div.admin.review input { 32 | padding: 5px 10px; 33 | margin-right: 10px; 34 | font-size: 110%; 35 | } 36 | 37 | div.admin.review span.reviewed_at { 38 | 39 | } 40 | 41 | div.admin.review a.edit { 42 | font-size: 80%; 43 | } 44 | 45 | div.admin.review div.review_message { 46 | margin-top: 15px; margin-bottom: 0; 47 | } 48 | 49 | div.admin.review div.review_message p {margin-bottom: 5px;} 50 | 51 | div.admin.review label.review_message { 52 | display: block; 53 | margin-top: 10px; 54 | } 55 | 56 | div.admin.review textarea.review_message { 57 | display: block; 58 | margin: 10px 0; 59 | height: 100px; width: 80%; 60 | } 61 | 62 | div.admin_error { 63 | margin-bottom: 40px; 64 | padding: 5px 10px; 65 | margin-top: -20px; 66 | } 67 | 68 | #admin-politician ul li {margin: 10px; padding: 5px; width: 420px; } 69 | 70 | #admin-politician img { margin-top: -20px; } 71 | 72 | #admin-politician ul li span.formfield { float: right; } 73 | 74 | #admin-politician ul li span.formfield button.expander { 75 | position: relative; 76 | left: 20px; 77 | width: 20px; 78 | height: 25px; 79 | margin-left: -22px; 80 | padding: 0; 81 | display: inline-block; 82 | font-size: 16px; 83 | } 84 | 85 | #admin-politician ul li div.expandable { 86 | padding: 3px; 87 | margin: 0; 88 | border-width: 0; 89 | } 90 | 91 | #admin-politician ul.suggestions li.suggestion { 92 | padding: 0; 93 | margin: 3px 0 5px 0; 94 | height: 80px; 95 | width: 100%; 96 | border-width: 0; 97 | background: transparent; 98 | } 99 | 100 | #admin-politician ul.suggestions li.suggestion span.name { 101 | vertical-align: top; 102 | font-size: 16px; 103 | text-align: right; 104 | } 105 | 106 | #admin-politician ul.suggestions li.suggestion a.identifier { 107 | vertical-align: bottom; 108 | font-size: 16px; 109 | text-align: right; 110 | white-space: nowrap; 111 | overflow: visible; 112 | } 113 | 114 | #admin-politician ul.suggestions li.suggestion img.photo { 115 | height: 80px; 116 | width: 66px; 117 | margin: 0 6px 0 0; 118 | padding: 0; 119 | border-width: 0; 120 | } 121 | #admin-politician ul.suggestions li.suggestion div.right { 122 | float: right; 123 | width: 340px; 124 | } 125 | 126 | #admin-politician span#name {font-size: 1.2em; font-weight: bold;} 127 | 128 | #admin-politician ul li button { text-indent: 0; border: 1px solid; padding: 5px; } 129 | 130 | #s_input, #suffix_input { width:20px; } 131 | #state { width: 20px ;} 132 | #mn_input, #middle_name_input { width: 30px; } 133 | 134 | #admin-politician #first_name, #admin-politician #middle_name, #admin-politician #last_name, #admin-politician #suffix { float: left; margin-left: 3px;margin-right: 3px} 135 | 136 | #admin-politician #first_name_input, #admin-politician #middle_name_input, #admin-politician #last_name_input, #admin-politician #suffix_input { float: left; margin-left: 3px;margin-right: 3px} 137 | 138 | #admin-list { font-size: 1.2em;} 139 | 140 | #admin-list h1 {width: auto; display: block ; float:none; } 141 | 142 | input { margin: 15px 0; } 143 | 144 | span.error { display: block; } 145 | #admin-politician input { margin: 0} 146 | input.related { width: 250px; } 147 | input.state { width: 30px;} 148 | 149 | table#queue-list { margin-top: 30px; } 150 | table#queue-list tr td:first-child { padding-right: 1em; } 151 | td.numeric { text-align: right; } 152 | 153 | div#annual-report h3 { 154 | margin: 3em 1em 1em 0; 155 | } 156 | div#annual-report ul { 157 | margin: 3em 0 0 0; 158 | } 159 | div#annual-report ol { 160 | margin: 0 0 0 3em; 161 | } 162 | div#party-tweeting table { 163 | border-collapse: collapse; 164 | } 165 | div#party-tweeting table th { 166 | padding: 0.3em; 167 | } 168 | div#party-tweeting table td { 169 | border: 1px solid gray; 170 | text-align: right; 171 | padding: 0.3em; 172 | } 173 | div#party-tweeting table td:first-child { 174 | text-align: left; 175 | } 176 | 177 | 178 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | *= stub propublica_base/base 14 | *= stub propublica_base/reset 15 | *= stub propublica_base/propublica-text 16 | *= stub propublica_base/master 17 | *= stub propublica_base/print 18 | *= stub propublica_base/woland 19 | */ -------------------------------------------------------------------------------- /app/assets/stylesheets/propublica_base/README.md: -------------------------------------------------------------------------------- 1 | <%= stylesheet_link_tag "propublica_base/base", :media => "all" %> 2 | <%= stylesheet_link_tag "propublica_base/master", :media => "screen" %> 3 | <%= stylesheet_link_tag "propublica_base/print", :media => "print" %> 4 | <%= stylesheet_link_tag "propublica_base/woland", :media => "screen and (max-width: 480px)" %> 5 | 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/propublica_base/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require 'propublica_base/reset' 14 | *= require 'propublica_base/propublica-text' 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /app/assets/stylesheets/propublica_base/propublica-text.css: -------------------------------------------------------------------------------- 1 | body{font:13px/1.23 Helvetica,Arial,sans-serif} 2 | a:focus{outline:1px dotted} 3 | hr{border:0 #ccc solid;border-top-width:1px;clear:both;height:0} 4 | h1,h2,h3,h4,h5,h6,strong{font-weight:bold;} 5 | abbr,acronym {border-bottom:1px dotted #000;cursor:help;} 6 | em{font-style:italic;} 7 | strong{font-weight:bold;} 8 | del{text-decoration:line-through;} 9 | ol{list-style:decimal} 10 | ol li{margin:0 0 5px 15px;} 11 | .article h1{font-size:23px} 12 | .article h2{font-size:19px} 13 | .article h3{font-size:17px} 14 | .article h4{font-size:15px} 15 | .article h5{font-size:13px} 16 | .article h6{font-size:13px} 17 | /*.article h1{font-size:25px} 18 | .article h2{font-size:23px} 19 | .article h3{font-size:21px} 20 | .article h4{font-size:19px} 21 | .article h5{font-size:17px} 22 | .article h6{font-size:15px}*/ 23 | .article > ol{list-style:decimal} 24 | .article > ul{list-style:disc } 25 | .article > li{margin:0 0 5px 15px;} 26 | .article p, 27 | .article dl, 28 | .article hr, 29 | .article h1, 30 | .article h2, 31 | .article h3, 32 | .article h4, 33 | .article h5, 34 | .article h6, 35 | .article ol, 36 | .article ul, 37 | .article pre, 38 | .article table, 39 | .article address, 40 | .article fieldset{margin-bottom:10px} 41 | .article p, 42 | .article blockquote, 43 | .article dl, 44 | .article ol, 45 | .article ul, 46 | .article table, 47 | .article address, 48 | .article fieldset{line-height:1.4;} 49 | 50 | .article-top .article, 51 | .article blockquote, 52 | .content-center .article p, .article-full .article > p, 53 | .content-center .article dl, .article-full .article > dl, 54 | .content-center .article ol, .article-full .article > ol, 55 | .content-center .article ul, .article-full .article > ul, 56 | .content-center .article table, .article-full .article > table, 57 | .content-center .article address, .article-full .article > address, 58 | .content-center .article fieldset, .article-full .article > fieldset{font-size:16px;} 59 | 60 | .article blockquote { margin:15px;padding: 0 1.5em; } 61 | -------------------------------------------------------------------------------- /app/assets/stylesheets/propublica_base/reset.css: -------------------------------------------------------------------------------- 1 | html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}:focus{outline:0}ins{text-decoration:none}del{text-decoration:line-through}table{border-collapse:collapse;border-spacing:0} 2 | -------------------------------------------------------------------------------- /app/assets/stylesheets/search.css.scss: -------------------------------------------------------------------------------- 1 | .search-box { 2 | width: 100%; 3 | padding: 10px 20px; 4 | background: #e9f0f8; 5 | -moz-box-sizing: border-box; 6 | -webkit-box-sizing: border-box; 7 | box-sizing: border-box; 8 | display: inline-block; 9 | margin-bottom: 10px; 10 | // border-top: 2px solid #CCC; 11 | 12 | h3 { 13 | font-family: "Sentinel A", "Sentinel B", Georgia, serif; 14 | line-height: 1.4em; 15 | font-size: 1.5em; 16 | padding: 0 0 8px; 17 | } 18 | 19 | .examples { 20 | font-family: 'Atlas Grotesk Web', Helvetica, sans-serif; 21 | margin: 10px 0 0; 22 | font-size: 11px; 23 | } 24 | } 25 | 26 | .home-search .search-box { 27 | border-top: 2px solid #ddd; 28 | margin-top: 20px; 29 | } 30 | 31 | .form-inline li { 32 | display: block; 33 | float: left; 34 | width: 220px; 35 | margin-right: 20px; 36 | } 37 | 38 | form#filter{ 39 | margin-bottom: 26px; 40 | } 41 | -------------------------------------------------------------------------------- /app/controllers/admin/admin_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::AdminController < ApplicationController 2 | layout "admin" 3 | 4 | before_filter :admin_only 5 | 6 | protected 7 | 8 | def admin_only 9 | request_url = URI.parse(request.url) 10 | status_json_path = url_for(:action => 'status', 11 | :controller => 'system', 12 | :format => 'json', 13 | :only_path => true) 14 | status_json = request_url.path == status_json_path 15 | 16 | ips = (request.env['HTTP_X_FORWARDED_FOR'] or request.remote_ip).split(',') 17 | ips = Set::new ips 18 | monitoring_ips = Set::new Settings[:monitoring_hosts] 19 | from_monitoring_host = (ips.intersection(monitoring_ips).size > 0) 20 | monitoring_request = (status_json and from_monitoring_host) 21 | 22 | unless (params[:format] == "rss" or monitoring_request) 23 | authenticate_or_request_with_http_basic do |username, password| 24 | username == Settings[:admin][:username] and password == Settings[:admin][:password] 25 | end 26 | end 27 | end 28 | 29 | helper_method :latest_tweet 30 | def latest_tweet 31 | @latest_tweet ||= Tweet.in_order.first 32 | end 33 | 34 | helper_method :latest_deleted_tweet 35 | def latest_deleted_tweet 36 | @latest_deleted_tweet ||= DeletedTweet.in_order.first 37 | end 38 | 39 | helper_method :current_admin_rss 40 | def current_admin_rss 41 | "#{request.url}/#{Settings[:rss_secret]}.rss" 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/controllers/admin/offices_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::OfficesController < Admin::AdminController 2 | 3 | def list 4 | @offices = Office.all 5 | respond_to do |format| 6 | format.html {render} 7 | end 8 | end 9 | def add 10 | respond_to do |format| 11 | format.html {render} 12 | end 13 | 14 | end 15 | 16 | def save 17 | if params[:obj_id] then 18 | office = Office.find(params[:obj_id].to_i) 19 | office.title = params[:title] 20 | office.abbreviation = params[:abbreviation] 21 | else 22 | office = Office.new(:title => params[:title], :abbreviation => params[:abbreviation]) 23 | end 24 | office.save() 25 | redirect_to "/admin/offices/" 26 | 27 | end 28 | 29 | def edit 30 | @office = Office.find(params[:id]) 31 | respond_to do |format| 32 | format.html {render} 33 | end 34 | end 35 | 36 | end 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/controllers/admin/politicians_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::PoliticiansController < Admin::AdminController 2 | def admin_list 3 | @politicians = Politician.all 4 | respond_to do |format| 5 | format.html { render } 6 | end 7 | end 8 | 9 | def admin_user 10 | begin 11 | @politician = Politician.find(params[:id]) 12 | rescue ActiveRecord::RecordNotFound 13 | @politician = Politician.where(:user_name => params[:id]).first 14 | if @politician.nil? 15 | raise('not found') 16 | end 17 | end 18 | 19 | @parties = Party.all 20 | @offices = Office.all 21 | @account_types = AccountType.all 22 | @related = @politician.get_related_politicians().sort_by(&:user_name) 23 | 24 | @unmoderated = DeletedTweet.where(:reviewed=>false, :politician_id => @politician).length 25 | 26 | respond_to do |format| 27 | format.html { render } 28 | end 29 | end 30 | 31 | def new_user 32 | @parties = Party.all 33 | @offices = Office.all 34 | @account_types = AccountType.all 35 | 36 | respond_to do |format| 37 | format.html { render } 38 | end 39 | end 40 | 41 | def get_twitter_id 42 | require 'twitter' 43 | t = Twitter.user(params[:screen_name]) 44 | @twitter_id = t.id 45 | respond_to do |format| 46 | format.json { render } 47 | end 48 | end 49 | 50 | def save_user 51 | if params[:user_name].to_s == '' 52 | flash[:error] = "You must specify a twitter username." 53 | return redirect_to :back 54 | elsif params[:twitter_id].to_s == '' 55 | flash[:error] = "Could not find the numeric twitter ID for twitter user #{params[:user_name]}" 56 | return redirect_to :back 57 | end 58 | 59 | if params[:id] == '0' then 60 | existing = Politician.where(:user_name => params[:user_name]) 61 | if existing.count == 0 62 | #it's a new add 63 | pol = Politician.create(:twitter_id => params[:twitter_id], 64 | :user_name => params[:user_name]) 65 | else 66 | flash[:error] = "We already track @#{params[:user_name]}" 67 | pol = nil 68 | end 69 | else 70 | pol = Politician.find(params[:id]) || raise("not found") 71 | pol.user_name = params[:user_name] 72 | end 73 | 74 | if not pol.nil? 75 | pol.party = Party.where(:id => params[:party_id]).first 76 | pol.status = params[:status] 77 | if params[:account_type_id] == '0' then 78 | pol.account_type = nil 79 | else 80 | pol.account_type = AccountType.where(:id => params[:account_type_id]).first 81 | end 82 | if params[:office_id] == '0' then 83 | pol.office = nil 84 | else 85 | pol.office = Office.where(:id => params[:office_id]).first 86 | end 87 | 88 | pol.update_attributes(params) 89 | 90 | pol.save! 91 | pol.reset_avatar 92 | end 93 | 94 | if params[:unapprove_all] and params[:unapprove_all] == 'on' then 95 | unmod = DeletedTweet.where(:reviewed=>false, :politician_id => pol) 96 | unmod.each do |utweet| 97 | utweet.approved = 0 98 | utweet.review_message = "Bulk unapproved in admin" 99 | utweet.reviewed = 1 100 | utweet.reviewed_at = Time.now 101 | utweet.save() 102 | 103 | end 104 | end 105 | 106 | if params[:related] then 107 | requested_names = Set.new(params[:related].split(',') 108 | .map(&:strip) 109 | .reject{ |name| name == '' }) 110 | existing_names = Set.new(pol.get_related_politicians.map(&:user_name)) 111 | pol.remove_related_politicians (existing_names - requested_names) 112 | pol.add_related_politicians (requested_names - existing_names) 113 | end 114 | 115 | redirect_to :back 116 | end 117 | 118 | end 119 | -------------------------------------------------------------------------------- /app/controllers/admin/reports_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::ReportsController < Admin::AdminController 2 | layout "admin" 3 | 4 | before_filter :admin_only 5 | 6 | def annual 7 | earliest_tweet_timestamp = DeletedTweet.minimum(:created) 8 | if earliest_tweet_timestamp.nil? 9 | return not_found 10 | end 11 | @year = (params[:year] && params[:year].to_i) || Date.today.year 12 | if earliest_tweet_timestamp.year > @year 13 | return not_found 14 | end 15 | 16 | @tweet_tally = Tweet.joins(:politician).in_year(@year).count 17 | @delete_tally = DeletedTweet.joins(:politician).in_year(@year).count 18 | @approval_tally = DeletedTweet.joins(:politician).in_year(@year).where(:approved => true).count 19 | @approval_pct = (@delete_tally.zero? && 0) || (@approval_tally * 100 / @delete_tally) 20 | @observed_account_tally = Tweet.joins(:politician).in_year(@year).group(:politician_id).order(nil).count.length 21 | 22 | @tweets_per_account = Tweet.joins(:politician).in_year(@year).group(:politician_id).count 23 | @deletes_per_account = DeletedTweet.joins(:politician).in_year(@year).group(:politician_id).count 24 | @twoops_per_account = DeletedTweet.joins(:politician).in_year(@year).where(:approved => true).group(:politician_id).count 25 | @tweeting_account_tally = @tweets_per_account.length 26 | @deleting_account_tally = @deletes_per_account.length 27 | @deleting_account_pct = (@observed_account_tally.zero? && 0) || (@deleting_account_tally * 100 / @observed_account_tally) 28 | @twooping_account_tally = @twoops_per_account.length 29 | 30 | @top_tweeters = @tweets_per_account.sort_by(&:second).last(5).reverse.map{ |pol_id, cnt| [Politician.find(pol_id), cnt] } 31 | @top_deleters = @deletes_per_account.sort_by(&:second).last(5).reverse.map{ |pol_id, cnt| [Politician.find(pol_id), cnt] } 32 | @top_twoopers = @twoops_per_account.sort_by(&:second).last(5).reverse.map{ |pol_id, cnt| [Politician.find(pol_id), cnt] } 33 | 34 | @top_twoops_rates = @deletes_per_account.select{ |pol_id, deletes| deletes > 5 }.map{ |pol_id, deletes| [pol_id, deletes.to_f * 100 / @tweets_per_account[pol_id].to_f] }.sort_by(&:second).last(10) 35 | @top_twoops_rates = @twoops_per_account.select{ |pol_id, deletes| deletes > 5 }.map{ |pol_id, twoops| [pol_id, twoops.to_f * 100 / @tweets_per_account[pol_id].to_f] }.sort_by(&:second).last(10) 36 | @top_twoopsters = @top_twoops_rates.reverse.map{ |pol_id, pct| [Politician.find(pol_id), pct.round] } 37 | 38 | @tweets_per_party = Tweet.joins(:politician).in_year(@year).group(:party_id).count 39 | @deletes_per_party = DeletedTweet.joins(:politician).in_year(@year).group(:party_id).count 40 | @twoops_per_party = DeletedTweet.joins(:politician).in_year(@year).where(:approved => true).group(:party_id).count 41 | @parties = Party.find(@tweets_per_party.keys) 42 | 43 | @tweeting_accounts_per_party = Tweet.joins(:politician).in_year(@year).group(:party_id).count(:politician_id, :distinct => true) 44 | @deleting_accounts_per_party = DeletedTweet.joins(:politician).in_year(@year).group(:party_id).count(:politician_id, :distinct => true) 45 | @twooping_accounts_per_party = DeletedTweet.joins(:politician).in_year(@year).where(:approved => true).group(:party_id).count(:politician_id, :distinct => true) 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /app/controllers/admin/system_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::SystemController < Admin::AdminController 2 | include ApplicationHelper 3 | 4 | def status 5 | # Checks heartbeat files found in the configuration. 6 | # If the file exists and the timestamp is current, the worker 7 | # is running and can be restarted via the web interface. 8 | # If the file is missing (the worker failed to restart) or the 9 | # file exists but the timestamp is old (the process exited abnormally) 10 | # then the process will have to be restarted by the admin. 11 | 12 | expected_heartbeats = Settings[:heartbeats_expected] 13 | @worker_statuses = expected_heartbeats.map do |w| 14 | path = File.join(Settings[:heartbeats_directory], w) 15 | exists = File.exists? path 16 | traceback = nil 17 | started = nil 18 | if exists 19 | begin 20 | heartbeat_contents = File.new(path).read() 21 | meta = JSON.parse(heartbeat_contents) 22 | started = Time.parse(meta.fetch('started')) 23 | rescue JSON::ParserError => e 24 | traceback = heartbeat_contents 25 | end 26 | 27 | mtime = File.mtime(path) 28 | ago = (Time.now - mtime).floor 29 | if ago < 0 30 | status = 'restarting' 31 | elsif ago <= (Settings[:heartbeat_interval] * 1.10) # Allow 10% error 32 | status = 'running' 33 | else 34 | status = 'dead' 35 | end 36 | else 37 | status = 'dead' 38 | end 39 | { 40 | :worker => w, 41 | :status => status, 42 | :last_seen => ago.nil? ? nil : mtime.strftime("%Y-%m-%dT%H:%M:%S%z"), 43 | :started => started.nil? ? nil : started.strftime("%Y-%m-%dT%H:%M:%S%z"), 44 | :uptime => (status == 'running') ? duration_abbrev(mtime - started).to_s : nil, 45 | :traceback => started.nil? ? traceback : nil 46 | } 47 | end 48 | 49 | @last_tweet = Tweet.with_content.order("modified DESC").first 50 | 51 | @queue_stats = [] 52 | queues = Settings.fetch(:beanstalk_queues, nil).values 53 | if queues 54 | beanstalk = Beanstalk::Pool.new(['localhost:11300']) 55 | tubes = beanstalk.list_tubes.map {|k,v| v} .flatten 56 | @queue_stats = queues.map {|q| [ q, (q.in? tubes or nil and beanstalk.stats_tube(q)) ] } 57 | end 58 | 59 | respond_to do |format| 60 | format.html { render :template => "admin/system/status" } 61 | format.json { 62 | last_tweet_fmt = @last_tweet ? @last_tweet.format : nil 63 | render :json => { :workers => @worker_statuses, :last_tweet => last_tweet_fmt, :queue_stats => @queue_stats } 64 | } 65 | end 66 | end 67 | 68 | def restart 69 | if params[:worker].empty? 70 | flash[:error] = "No such worker!" 71 | return redirect_to :action => "status" 72 | end 73 | 74 | expected_heartbeats = Settings[:heartbeats_expected] 75 | if not expected_heartbeats.include? params[:worker] 76 | flash[:error] = "No such worker!" 77 | return redirect_to :action => "status" 78 | end 79 | 80 | path = File.join(Settings[:heartbeats_directory], params[:worker]) 81 | if File.exists? path 82 | future = Time.now + Settings[:heartbeat_interval] * 2 83 | File.utime future, future, path 84 | flash[:error] = "Worker restarting" 85 | return redirect_to :action => "status", :alert => "Worker restarting." 86 | else 87 | flash[:error] = "Worker is dead and it can't be restarted." 88 | return redirect_to :action => "status" 89 | end 90 | end 91 | 92 | def report 93 | if params[:worker].empty? 94 | flash[:error] = "Worker restarting" 95 | return redirect_to :action => "status" 96 | end 97 | 98 | flash[:error] = "Problem reported, thanks." 99 | return redirect_to :action => "status" 100 | end 101 | end 102 | 103 | -------------------------------------------------------------------------------- /app/controllers/admin/tweets_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::TweetsController < Admin::AdminController 2 | before_filter :load_tweet, :only => [:review, :message] 3 | 4 | # list either unreviewed 5 | def index 6 | @page = [params[:page].to_i, 1].max 7 | @politicians = Politician.active.all 8 | 9 | @tweets = DeletedTweet.in_order.where(:politician_id => @politicians) 10 | 11 | # filter to relevant subset of deleted tweets 12 | @tweets = @tweets.where :reviewed => params[:reviewed], :approved => params[:approved] 13 | 14 | # show unreviewed tweets oldest to newest 15 | if !params[:reviewed] 16 | @tweets = @tweets.reorder "modified ASC" 17 | end 18 | 19 | per_page = params[:per_page] ? params[:per_page].to_i : nil 20 | per_page ||= Tweet.per_page 21 | per_page = 200 if per_page > 200 22 | 23 | @tweets = @tweets.includes(:politician => [:party]).paginate(:page => @page, :per_page => per_page) 24 | @admin = true 25 | 26 | respond_to do |format| 27 | format.html # admin/tweets/index.html.erb 28 | format.rss do 29 | response.headers["Content-Type"] = "application/rss+xml; charset=utf-8" 30 | render "tweets/index" 31 | end 32 | end 33 | 34 | end 35 | 36 | 37 | # approve or unapprove a tweet, mark it as reviewed either way 38 | def review 39 | 40 | review_message = (params[:review_message] || "").strip 41 | 42 | if ["Approve", "Unapprove"].include?(params[:commit]) 43 | approved = (params[:commit] == "Approve") 44 | 45 | if !@tweet.reviewed? and approved and review_message.blank? 46 | flash[@tweet.id] = "You need to add a note about why you're approving this tweet." 47 | redirect_to params[:return_to] 48 | return false 49 | end 50 | 51 | @tweet.approved = approved 52 | @tweet.reviewed = true 53 | @tweet.reviewed_at = Time.now 54 | end 55 | 56 | if not review_message.blank? 57 | @tweet.review_message = review_message 58 | end 59 | 60 | @tweet.save! 61 | expire_action :controller => '/tweets', :action => :index 62 | 63 | redirect_to params[:return_to] 64 | end 65 | 66 | 67 | # filters 68 | 69 | def load_tweet 70 | unless params[:id] and (@tweet = DeletedTweet.find(params[:id])) 71 | render :nothing => true, :status => :not_found 72 | return false 73 | end 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | class ApplicationController < ActionController::Base 3 | protect_from_forgery 4 | 5 | # let tweet helper methods be available in the controller 6 | helper TweetsHelper 7 | 8 | before_filter :donor_banner 9 | 10 | rescue_from ActiveRecord::RecordNotFound, :with => :file_not_found 11 | 12 | def file_not_found 13 | respond_to do |format| 14 | format.html { render :file => "public/404.html", :status => 404} 15 | end 16 | end 17 | 18 | def donor_banner 19 | @donor_banner_enabled = Settings.fetch(:enable_donor_banner, false) 20 | end 21 | 22 | # needs to become more dynamic somehow 23 | def set_locale 24 | # not sure what this does 25 | I18n::Backend::Simple.send(:include, I18n::Backend::Flatten) 26 | I18n.locale = "en" 27 | end 28 | 29 | def enable_filter_form 30 | @states = Politician.where("state IS NOT NULL").pluck(:state).uniq 31 | @states = @states.sort 32 | 33 | @parties = Party.all 34 | @offices = Office.all 35 | 36 | @politicians = Politician.active 37 | 38 | #check for filters 39 | @filters = {'state' => nil, 'party' => nil, 'office' => nil } 40 | unless params.fetch('state', '').empty? 41 | @politicians = @politicians.where(:state => params[:state]) 42 | @filters['state'] = params[:state] 43 | end 44 | unless params.fetch('party', '').empty? 45 | party = Party.where(:name => params[:party])[0] 46 | @politicians = @politicians.where(:party_id => party) 47 | @filters['party'] = party.name 48 | end 49 | unless params.fetch('office', '').empty? 50 | @politicians = @politicians.where(:office_id => params[:office]) 51 | @filters['office'] = params[:office] 52 | end 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /app/controllers/errors_controller.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | class ErrorsController < ApplicationController 3 | include ApplicationHelper 4 | 5 | def not_found 6 | render "errors/not_found", :status => 404 7 | end 8 | 9 | def down 10 | # We don't generate a 500 here because this is simply for regenerating a 11 | # static error page. 12 | render "errors/status_5xx" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/controllers/parties_controller.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | class PartiesController < ApplicationController 3 | 4 | include ApplicationHelper 5 | before_filter :enable_filter_form 6 | 7 | def show 8 | @per_page_options = [20, 50] 9 | @per_page = closest_value((params.fetch :per_page, 0).to_i, @per_page_options) 10 | @page = [params[:page].to_i, 1].max 11 | 12 | @politicians = Party.where(:name => params[:name]).first.politicians.active.map {|politician| politician.id} 13 | @tweets = DeletedTweet.includes(:politician => [:party]).where(:politician_id => @politicians, :approved => true).paginate(:page => params[:page], :per_page => Tweet.per_page) 14 | render "tweets/index" 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/politicians_controller.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | class PoliticiansController < ApplicationController 3 | 4 | include ApplicationHelper 5 | 6 | caches_action :show, expires_in: 5.minutes, if: proc { (params.keys - ['format', 'action', 'controller']).empty? } 7 | 8 | def show 9 | @per_page_options = [20, 50] 10 | @per_page = closest_value((params.fetch :per_page, 0).to_i, @per_page_options) 11 | @page = [params[:page].to_i, 1].max 12 | 13 | @politician = Politician.active.where(user_name: params[:user_name]).first 14 | raise ActiveRecord::RecordNotFound unless @politician 15 | 16 | # need to get the latest tweet to get correct bio. could do with optimization :) 17 | @latest_tweet = Tweet.in_order.where(politician_id: @politician.id).first 18 | 19 | @related = @politician.get_related_politicians().to_a.sort_by(&:user_name) 20 | @accounts = [@politician] + @related 21 | 22 | @tweet_map = {} 23 | @accounts.each do |ac| 24 | @tweet_map[ac.user_name] = ac.twoops.in_order.includes(:tweet_images).paginate(page: @page, per_page: @per_page) 25 | end 26 | 27 | if @tweet_map.size == 1 28 | @tweet_map['all'] = @tweets = @tweet_map.values.first 29 | else 30 | @tweet_map['all'] = @tweets = DeletedTweet.in_order.includes(:tweet_images).where(politician_id: @accounts.map(&:id), approved: true).paginate(page: @page, per_page: @per_page) 31 | end 32 | 33 | respond_to do |format| 34 | format.html { render } # politicians/show 35 | format.rss do 36 | response.headers["Content-Type"] = "application/rss+xml; charset=utf-8" 37 | render "tweets/index" 38 | end 39 | end 40 | end 41 | 42 | before_filter :enable_filter_form 43 | def all 44 | 45 | @per_page_options = [20, 50] 46 | @per_page = closest_value((params.fetch :per_page, 0).to_i, @per_page_options) 47 | @page = [params[:page].to_i, 1].max 48 | 49 | @filter_action = "/users" 50 | 51 | respond_to do |format| 52 | format.html { 53 | #get all politicians that we're showing 54 | @politicians = @politicians.order('last_name').where(:status => [1, 4]) 55 | @politicians = @politicians.paginate(:page => params[:page], :per_page => @per_page) 56 | render 57 | } 58 | format.csv { 59 | @politicians = @politicians.order('last_name') 60 | render :csv => @politicians, :filename => 'users' 61 | } 62 | end 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /app/controllers/tweets_controller.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | class TweetsController < ApplicationController 3 | # GET /tweets 4 | # GET /tweets.xml 5 | 6 | require 'RMagick' 7 | include ApplicationHelper 8 | 9 | caches_action :index, 10 | :expires_in => 30.minutes, 11 | :if => proc { (params.keys - ['format', 'action', 'controller']).empty? } 12 | 13 | caches_action :thumbnail 14 | 15 | before_filter :enable_filter_form 16 | 17 | def index 18 | @filter_action = "/" 19 | 20 | if params.has_key?(:see) && params[:see] == :all 21 | @tweets = Tweet.in_order 22 | else 23 | @tweets = DeletedTweet.in_order 24 | end 25 | 26 | @tweets = @tweets.where(:politician_id => @politicians) 27 | tweet_count = 0 #@tweets.count 28 | 29 | if params.has_key?(:q) and params[:q].present? 30 | # Rails prevents injection attacks by escaping things passed in with ? 31 | @query = params[:q] 32 | query = "%#{@query}%" 33 | @search_pols = Politician.where("MATCH(user_name, first_name, middle_name, last_name) AGAINST (?)", query) 34 | @tweets = @tweets.where("content like ? or deleted_tweets.user_name like ? or politician_id in (?)", query, query, @search_pols) 35 | 36 | end 37 | 38 | # only approved tweets 39 | @tweets = @tweets.where(:approved => true) 40 | 41 | @per_page_options = [20, 50] 42 | @per_page = closest_value((params.fetch :per_page, 0).to_i, @per_page_options) 43 | @page = [params[:page].to_i, 1].max 44 | 45 | @tweets = @tweets.includes(:tweet_images, :politician => [:party]).paginate(:page => params[:page], :per_page => @per_page) 46 | 47 | respond_to do |format| 48 | format.html # index.html.erb 49 | format.rss do 50 | response.headers["Content-Type"] = "application/rss+xml; charset=utf-8" 51 | render 52 | end 53 | format.json { render :json => {:meta => {:count => tweet_count}, :tweets => @tweets.map{|tweet| tweet.format } } } 54 | end 55 | end 56 | 57 | # GET /tweets/1 58 | # GET /tweets/1.xml 59 | def show 60 | @tweet = DeletedTweet.includes(:politician).find(params[:id]) 61 | 62 | if (@tweet.politician.status != 1 and @tweet.politician.status != 4) or not @tweet.approved 63 | not_found 64 | end 65 | 66 | respond_to do |format| 67 | format.html # show.html.erb 68 | format.xml { render :xml => @tweet } 69 | format.json { render :json => @tweet.format } 70 | end 71 | end 72 | 73 | def thumbnail 74 | tweet = Tweet.find(params[:tweet_id]) 75 | if not tweet 76 | not_found 77 | end 78 | 79 | images = tweet.tweet_images.all 80 | if not images 81 | not_found 82 | end 83 | 84 | filename = "#{params[:basename]}.#{params[:format]}" 85 | image = images.select do |img| 86 | img.filename == filename 87 | end .first 88 | 89 | if not image 90 | not_found 91 | end 92 | 93 | resp = HTTParty.get(image.url) 94 | img = Magick::Image.from_blob(resp.body) 95 | layer0 = img[0] 96 | aspect_ratio = layer0.columns.to_f / layer0.rows.to_f 97 | if aspect_ratio > 1.0 98 | new_width = 150 99 | new_height = layer0.rows.to_f / aspect_ratio 100 | else 101 | new_width = layer0.columns.to_f / aspect_ratio 102 | new_height = 150 103 | end 104 | thumb = layer0.resize_to_fit(new_width, new_height) 105 | send_data(thumb.to_blob, 106 | :disposition => 'inline', 107 | :type => resp.headers.fetch('content-type', 'application/octet-stream'), 108 | :filename => filename) 109 | end 110 | 111 | end 112 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module ApplicationHelper 3 | 4 | def light_format(string) 5 | return "" unless string.present? 6 | string = simple_format h(string) 7 | auto_link string 8 | end 9 | 10 | def url_with_params(style, h) 11 | url = URI.parse(request.url) 12 | unless h.empty? 13 | params = Rack::Utils.parse_query(url.query) 14 | params.update h 15 | url.query = params.to_param 16 | end 17 | case style 18 | when :URI 19 | return url.path + '?' + url.query 20 | when :URL 21 | return url.to_s 22 | else 23 | return url.to_s 24 | end 25 | end 26 | 27 | def bound_value (value, range) 28 | return range.first if value < range.first 29 | return range.last if value > range.last 30 | value 31 | end 32 | 33 | def closest_value value, list 34 | return list.min { |a,b| (a-value).abs <=> (b-value).abs } 35 | end 36 | 37 | MINUTE = 60 38 | HOUR = MINUTE * 60 39 | DAY = HOUR * 24 40 | WEEK = DAY * 7 41 | def duration_abbrev(seconds) 42 | 43 | (weeks, seconds) = seconds.divmod WEEK 44 | (days, seconds) = seconds.divmod DAY 45 | (hours, seconds) = seconds.divmod HOUR 46 | (minutes, seconds) = seconds.divmod MINUTE 47 | seconds = seconds.floor 48 | 49 | clauses = [ 50 | ("#{weeks}w" if weeks > 0), 51 | ("#{days}d" if days > 0), 52 | ("#{hours}h" if hours > 0), 53 | ("#{minutes}m" if minutes > 0), 54 | ("#{seconds}s" if seconds > 0) 55 | ] 56 | clauses.join('') 57 | end 58 | 59 | def relative_time(start_time) 60 | diff_seconds = Time.now - start_time 61 | case diff_seconds 62 | when 0 .. 59 63 | "#{diff_seconds} seconds ago" 64 | when 60 .. (3600-1) 65 | minutes = (diff_seconds/60).round 66 | "#{minutes} minutes ago" 67 | when 3600 .. (3600*24-1) 68 | hours = (diff_seconds/3600).round 69 | "#{hours} hours ago" 70 | when (3600*24) .. (3600*24*30) 71 | days = (diff_seconds/(3600*24)).round 72 | "#{days} days ago" 73 | else 74 | start_time.strftime("%m/%d/%Y") 75 | end 76 | end 77 | 78 | end 79 | 80 | -------------------------------------------------------------------------------- /app/helpers/tweets_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module TweetsHelper 3 | def default_avatar_url (pol) 4 | if pol.female? 5 | "/images/avatar_missing_female.png" 6 | else 7 | "/images/avatar_missing_male.png" 8 | end 9 | end 10 | 11 | def office_title_for (pol) 12 | pol.office.nil? ? '' : pol.office.title 13 | end 14 | 15 | def office_abbr_for (pol) 16 | pol.office.nil? ? '' : pol.office.abbreviation 17 | end 18 | 19 | def party_name_for (pol) 20 | pol.party.nil? ? '' : pol.party.name.upcase 21 | end 22 | 23 | def format_user_name(tweet_content) 24 | tweet_content.gsub(/(@(\w+))/, %Q{\\1}) 25 | end 26 | 27 | def format_hashtag(tweet_content) 28 | tweet_content.gsub(/(#(\w+))/, %Q{\\1}) 29 | end 30 | 31 | def format_retweet_prefix (content, user_name) 32 | "RT @#{user_name}: #{content}" 33 | end 34 | 35 | def format_tweet(tweet) 36 | if tweet.retweeted_id.nil? 37 | content = tweet.content 38 | else 39 | content = format_retweet_prefix(tweet.retweeted_content, 40 | tweet.retweeted_user_name) 41 | end 42 | content = format_hashtag(content) 43 | content = format_user_name(content) 44 | content = auto_link(content, :html => { :target => '_blank' }) 45 | end 46 | 47 | def twitter_url (tweet_user_name, tweet_id) 48 | "http://www.twitter.com/#{tweet_user_name}/status/#{tweet_id}" 49 | end 50 | 51 | def byline(tweet, html = true) 52 | if (Time.now - tweet.modified).to_i > (60 * 60 * 24 * 365) 53 | tweet_time = tweet.modified.strftime("%l:%M %p") 54 | tweet_date = tweet.modified.strftime("%d %b %y") # 03 Jun 12 55 | tweet_when = "at #{tweet_time} on #{tweet_date}" 56 | elsif (Time.now - tweet.modified).to_i > (60 * 60 * 24) 57 | tweet_time = tweet.modified.strftime("%l:%M %p") 58 | tweet_date = tweet.modified.strftime("%d %b") # 03 Jun 59 | tweet_when = "at #{tweet_time} on #{tweet_date}" 60 | else 61 | since_tweet = time_ago_in_words tweet.modified 62 | tweet_when = "#{since_tweet} ago" 63 | end 64 | delete_delay = (tweet.modified - tweet.created).to_i 65 | 66 | delay = if delete_delay > (60 * 60 * 24 * 7) 67 | "after #{pluralize(delete_delay / (60 * 60 * 24 * 7), "week")}" 68 | elsif delete_delay > (60 * 60 * 24) 69 | "after #{pluralize(delete_delay / (60 * 60 * 24), "day")}" 70 | elsif delete_delay > (60 * 60) 71 | "after #{pluralize(delete_delay / (60 * 60), "hour")}" 72 | elsif delete_delay > 60 73 | "after #{pluralize(delete_delay / 60, "minute")}" 74 | elsif delete_delay > 1 75 | "after #{pluralize delete_delay, "second"}" 76 | else 77 | "immediately" 78 | end 79 | 80 | if tweet.retweeted_id.nil? 81 | rt_text = "" 82 | else 83 | orig_url = twitter_url(tweet.retweeted_user_name, tweet.retweeted_id) 84 | rt_text = "Original tweet by @#{tweet.retweeted_user_name}." 85 | end 86 | 87 | if html 88 | source = tweet.details["source"].to_s.html_safe 89 | byline = "#{tweet.details['user']['name']}".html_safe 90 | byline += t(:byline, 91 | :scope => [:politwoops, :tweets], 92 | :retweet => rt_text, 93 | :when => tweet_when, 94 | :what => source, 95 | :delay => delay).html_safe 96 | byline 97 | else 98 | t :byline_text, :scope => [:politwoops, :tweets], :when => tweet_when, :delay => delay 99 | end 100 | end 101 | 102 | def rss_date(time) 103 | time.strftime "%a, %d %b %Y %H:%M:%S %z" 104 | end 105 | 106 | end 107 | -------------------------------------------------------------------------------- /app/models/account_link.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | class AccountLink < ActiveRecord::Base 3 | 4 | belongs_to :link, :class_name => "Politician" 5 | 6 | end 7 | -------------------------------------------------------------------------------- /app/models/account_type.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | class AccountType < ActiveRecord::Base 3 | has_many :politicians 4 | end 5 | -------------------------------------------------------------------------------- /app/models/deleted_tweet.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | class DeletedTweet < Tweet 3 | self.table_name="deleted_tweets" 4 | end 5 | -------------------------------------------------------------------------------- /app/models/office.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | class Office < ActiveRecord::Base 3 | has_many :politicians 4 | default_scope { order("title")} 5 | end 6 | -------------------------------------------------------------------------------- /app/models/party.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | class Party < ActiveRecord::Base 3 | has_many :politicians 4 | 5 | def tweets 6 | Tweet.joins(:politician).where(:politicians => { :party_id => self.id }) 7 | end 8 | 9 | def deleted_tweets 10 | DeletedTweet.joins(:politician).where(:politicians => { :party_id => self.id }) 11 | end 12 | 13 | def twoops 14 | DeletedTweet.joins(:politician).where(:approved => true, :politicians => { :party_id => self.id }) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/politician.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "open-uri" 3 | require 'gender_detector' 4 | 5 | class Politician < ActiveRecord::Base 6 | CollectingAndShowing = 1 7 | CollectingNotShowing = 2 8 | NotCollectingOrShowing = 3 9 | NotCollectingButShowing = 4 10 | 11 | has_attached_file :avatar, { :path => ':base_path/avatars/:filename', 12 | :url => "/images/avatars/:filename", 13 | :default_url => '' } 14 | 15 | belongs_to :party 16 | 17 | belongs_to :office 18 | 19 | belongs_to :account_type 20 | 21 | has_many :tweets 22 | has_many :deleted_tweets 23 | 24 | has_many :account_links 25 | has_many :links, :through => :account_links 26 | 27 | #default_scope :order => 'user_name' 28 | 29 | scope :active, -> { where status: [1, 4]} 30 | scope :collecting, -> { where status: [CollectingAndShowing, CollectingNotShowing] } 31 | scope :showing, -> { where status: [CollectingAndShowing, NotCollectingButShowing] } 32 | 33 | validates_uniqueness_of :user_name, :case_sensitive => false 34 | 35 | comma do 36 | user_name 'user_name' 37 | twitter_id 'twitter_id' 38 | party :display_name => 'party_name' 39 | state 'state' 40 | office :title => 'office_title' 41 | account_type :name => 'account_type' 42 | first_name 'first_name' 43 | middle_name 'middle_name' 44 | last_name 'last_name' 45 | suffix 'suffix' 46 | status 'status' 47 | collecting? 'collecting' 48 | showing? 'showing' 49 | bioguide_id 'bioguide_id' 50 | opencivicdata_id 'opencivicdata_id' 51 | end 52 | 53 | def collecting? 54 | [CollectingAndShowing, CollectingNotShowing].include?(status) 55 | end 56 | 57 | def showing? 58 | [CollectingAndShowing, NotCollectingButShowing].include?(status) 59 | end 60 | 61 | def self.guess_gender(name) 62 | # Each SexMachine::Detector instance loads it's own copy of the data file. 63 | # Let's avoid going memory crazy. 64 | @_sexmachine__detector ||= GenderDetector.new 65 | @_sexmachine__detector.get_gender(name) 66 | end 67 | 68 | def guess_gender! 69 | gender_value = Politician.guess_gender(first_name) 70 | gender_map = { :male => 'M', :female => 'F' } 71 | gender_value = gender_map[gender_value] 72 | if gender_value 73 | self.gender = gender_value 74 | save! 75 | end 76 | end 77 | 78 | def male? 79 | gender == 'M' 80 | end 81 | 82 | def female? 83 | gender == 'F' 84 | end 85 | 86 | def ungendered? 87 | !(male? || female?) 88 | end 89 | 90 | def self.ungendered 91 | where(:gender => 'U') 92 | end 93 | 94 | def full_name 95 | return [office && office.abbreviation, first_name, last_name, suffix].join(' ').strip 96 | end 97 | 98 | def add_related_politicians(other_names) 99 | other_names.each do |other_name| 100 | if not other_name.empty? && other_name != self.user_name 101 | other_pol = Politician.find_by_user_name(other_name) 102 | self.links << other_pol 103 | self.save! 104 | end 105 | end 106 | end 107 | 108 | def remove_related_politicians(other_names) 109 | other_names.each do |other_name| 110 | if not other_name.empty? && other_name != self.user_name 111 | other_pol = Politician.find_by_user_name(other_name) 112 | AccountLink.where(:politician_id => self.id, 113 | :link_id => other_pol.id).destroy_all 114 | AccountLink.where(:link_id => self.id, 115 | :politician_id => other_pol.id).destroy_all 116 | end 117 | end 118 | end 119 | 120 | def get_related_politicians 121 | links = AccountLink.where("politician_id = ? or link_id = ?", self.id, self.id) 122 | 123 | politician_ids = links.flat_map{ |l| [l.politician_id, l.link_id] } 124 | .reject{ |pol_id| pol_id == self.id } 125 | Politician.where(:id => politician_ids) 126 | end 127 | 128 | def twoops 129 | deleted_tweets.where(:approved => true) 130 | end 131 | 132 | def reset_avatar(options = {}) 133 | begin 134 | twitter_user = $twitter.user(user_name) 135 | image_url = twitter_user.profile_image_url(:bigger) 136 | 137 | force_reset = options.fetch(:force, false) 138 | 139 | if profile_image_url.nil? || (image_url != profile_image_url) || (profile_image_url != avatar.url) || force_reset 140 | uri = URI::parse(image_url) 141 | extension = File.extname(uri.path) 142 | 143 | uri.open do |remote_file| 144 | Tempfile.open(["#{self.twitter_id}_", extension]) do |tmpfile| 145 | tmpfile.puts remote_file.read().force_encoding('UTF-8') 146 | self.avatar = tmpfile 147 | self.profile_image_url = image_url 148 | self.save! 149 | end 150 | end 151 | end 152 | return [true, nil] 153 | rescue Twitter::Error::Forbidden => e 154 | return [false, e.to_s] 155 | rescue Twitter::Error::NotFound 156 | return [false, "No such user name: #{user_name}"] 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /app/models/tweet.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | class Tweet < ActiveRecord::Base 3 | belongs_to :politician 4 | 5 | has_many :tweet_images, :foreign_key => "tweet_id" 6 | 7 | scope :with_content, -> { where.not content: nil} 8 | scope :retweets, -> { where.not retweeted_id: nil} 9 | 10 | before_save :extract_retweet_fields 11 | 12 | cattr_reader :per_page 13 | @@per_page = 10 14 | 15 | def self.in_order 16 | includes(:politician).order('modified DESC') 17 | end 18 | 19 | def self.latest 20 | order('created DESC') 21 | end 22 | 23 | def self.deleted 24 | where(deleted: 1).where.not(content: nil) 25 | end 26 | 27 | def self.in_year(year) 28 | where("created >= #{Date.new(year, 1, 1)}").where("created <= #{Date.new(year, 12, 31)}") 29 | end 30 | 31 | def self.random 32 | Tweet.find(Tweet.pluck(:id).shuffle.first) 33 | end 34 | 35 | def details 36 | JSON.parse(tweet) 37 | end 38 | 39 | def extract_retweeted_status 40 | return nil if tweet.nil? 41 | orig_obj = JSON::parse(tweet) rescue nil 42 | return nil if orig_obj.nil? 43 | return nil if not orig_obj.is_a?(Hash) 44 | return nil if orig_obj["retweeted_status"].nil? 45 | 46 | return orig_obj["retweeted_status"] 47 | end 48 | 49 | def extract_retweet_fields (options = {}) 50 | if retweeted_id.nil? || !options[:overwrite].nil? 51 | orig_hash = extract_retweeted_status 52 | if orig_hash 53 | self.retweeted_id = orig_hash["id"] 54 | self.retweeted_content = orig_hash["text"] 55 | self.retweeted_user_name = orig_hash["user"]["screen_name"] 56 | end 57 | end 58 | end 59 | 60 | def twitter_url 61 | "https://www.twitter.com/#{user_name}/status/#{id}" 62 | end 63 | 64 | def format 65 | { 66 | :created_at => created, 67 | :updated_at => modified, 68 | :id => (id and id.to_s), 69 | :politician_id => politician_id, 70 | :details => details, 71 | :content => content, 72 | :user_name => user_name 73 | } 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /app/models/tweet_image.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | class TweetImage < ActiveRecord::Base 3 | def filename 4 | File.basename(URI.parse(url).path) 5 | end 6 | def basename 7 | File.basename(filename, '.*') 8 | end 9 | def extension 10 | ext = File.extname(filename) 11 | if ext.length > 0 12 | ext = ext[1..-1] 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/services/export/export_fixtures.rb: -------------------------------------------------------------------------------- 1 | module Export 2 | class ExportFixtures < ServiceBase 3 | include Virtus.model 4 | attribute :models 5 | attribute :export_dir, String 6 | 7 | def call 8 | FileUtils.mkdir_p export_dir 9 | models.each do |mdl| 10 | path = File.join(export_dir, "#{mdl.class.table_name}.yml") 11 | key_prefix = mdl.class.table_name.singularize 12 | File.open(path, 'w') do |outf| 13 | key = "#{key_prefix}_#{mdl.id}" 14 | outf.write({key => mdl.attributes}.to_yaml) 15 | end 16 | end 17 | 18 | success "Exported to #{export_dir}" 19 | end 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /app/services/export/export_tweet_subgraph_fixtures.rb: -------------------------------------------------------------------------------- 1 | module Export 2 | class ExportTweetSubgraphFixtures < ServiceBase 3 | include Virtus.model 4 | attribute :tweet, Tweet 5 | attribute :export_dir, String 6 | 7 | def call 8 | deleted_tweet = DeletedTweet.where(:id => tweet.id).first 9 | pol = tweet.politician 10 | # Call accessors now to fail early 11 | models = [tweet, deleted_tweet, pol, pol.party, pol.office, pol.account_type].compact 12 | ExportFixtures.call(:models => models, :export_dir => export_dir) 13 | end 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /app/services/failure.rb: -------------------------------------------------------------------------------- 1 | class Failure 2 | attr_reader :message 3 | 4 | def initialize (error=nil) 5 | if error.is_a? Exception 6 | @exception = error 7 | @message = error.to_s 8 | else 9 | @exception = nil 10 | @message = error 11 | end 12 | end 13 | 14 | def success? 15 | false 16 | end 17 | 18 | def to_s 19 | message.blank? and "Failed!" or "Failure: #{message}" 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /app/services/requeue_tweet.rb: -------------------------------------------------------------------------------- 1 | class RequeueTweet < ServiceBase 2 | include Virtus.model 3 | attribute :tweet, Tweet 4 | attribute :queue_name, String 5 | 6 | def call 7 | decoded = JSON.load(tweet.tweet) # Ensure that it can be decoded. 8 | if tweet.is_a? DeletedTweet 9 | decoded = { 'delete' => { 'status' => { 10 | 'id' => decoded['id'], 11 | 'id_str' => decoded['id_str'], 12 | 'user_id' => decoded['user']['id'], 13 | 'user_id_str' => decoded['user']['id_str'] 14 | } } } 15 | end 16 | beanstalk = Beanstalk::Pool.new(['localhost:11300']) 17 | beanstalk.use(queue_name) 18 | beanstalk.put(JSON.dump(decoded)) 19 | success "Requeued #{tweet.class.table_name.singularize.gsub('_', ' ')} #{tweet.id}" 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /app/services/service_base.rb: -------------------------------------------------------------------------------- 1 | class ServiceBase 2 | def self.call(*args) 3 | new(*args).call 4 | end 5 | 6 | def self.call_safely(*args) 7 | begin 8 | new(*args).call 9 | rescue => e 10 | Failure.new(e) 11 | end 12 | end 13 | 14 | def success (msg) 15 | Success.new(msg) 16 | end 17 | 18 | def failure (err) 19 | Failure.new(err) 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /app/services/success.rb: -------------------------------------------------------------------------------- 1 | class Success 2 | attr_reader :message 3 | 4 | def initialize (msg=nil) 5 | @message = msg 6 | end 7 | 8 | def success? 9 | true 10 | end 11 | 12 | def to_s 13 | message.blank? and "Success!" or "Successfully #{message.uncapitalize}" 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /app/views/admin/offices/add.html.erb: -------------------------------------------------------------------------------- 1 |

Add an Office Type

2 | 3 |
4 |
    5 |
  • 6 | Office Title: 7 |
  • 8 |
  • 9 | Abbreviation: 10 |
  • 11 |
  • 12 |
13 | 14 |
15 | -------------------------------------------------------------------------------- /app/views/admin/offices/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Edit Office

2 | 3 |
4 | 6 |
  • 7 | Office Title: 8 |
  • 9 |
  • 10 | Abbreviation: 11 |
  • 12 |
  • 13 | 14 | 15 |
    16 | -------------------------------------------------------------------------------- /app/views/admin/offices/list.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | 3 |

    List of Offices  Add One »

    4 | 5 | 13 |
    14 | -------------------------------------------------------------------------------- /app/views/admin/politicians/_identifier_suggestion_templates.html.erb: -------------------------------------------------------------------------------- 1 | 11 | 21 | 22 | -------------------------------------------------------------------------------- /app/views/admin/politicians/admin_list.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | Getchya spreadsheets! Getchya spreadsheets! 3 |
    4 | 5 |
    6 |

    List of Twoopsters  (add new)

    7 | 14 |
    15 | 16 | -------------------------------------------------------------------------------- /app/views/admin/politicians/admin_show.html.erb: -------------------------------------------------------------------------------- 1 |
      2 | <% @politicians.each do |pol| %> 3 |
    • 4 | <%=pol.user_name %> 5 |
    • 6 | <% end %> 7 |
    8 | 9 | -------------------------------------------------------------------------------- /app/views/admin/politicians/get_twitter_id.json.erb: -------------------------------------------------------------------------------- 1 | {"twitter_id": "<%= @twitter_id %>"} 2 | -------------------------------------------------------------------------------- /app/views/admin/politicians/new_user.html.erb: -------------------------------------------------------------------------------- 1 | <% flash.each do |cls, msg| %> 2 |
    3 | <%= msg %> 4 |
    5 | <% end %> 6 | 7 |

    Add a New Politician

    8 | 9 |
    10 | 11 |
      12 |
    • User Name: 13 | 14 |
    • 15 |
    • 16 | Display Name (first, mid, last, suffix):
      17 | 18 | 19 | 20 | 21 | 22 | 23 |
    • 24 |
    • 25 | Party: 26 | 33 | 34 |
    • 35 |
    • Status: 36 | 43 | 44 |
    • 45 | 46 |
    • 47 | State: 48 |
    • 49 | 50 |
    • Account Type: 51 | 59 | 60 |
    • 61 |
    • Office Held: 62 | 70 | 71 |
    • 72 |
    • Gender: 73 | 74 | <%= radio_button_tag(:gender, 'M') %> 75 | <%= label_tag(:gender_M, 'Male') %> 76 | <%= radio_button_tag(:gender, 'F') %> 77 | <%= label_tag(:gender_F, 'Female') %> 78 | <%= radio_button_tag(:gender, 'U') %> 79 | <%= label_tag(:gender_U, 'Unspecified') %> 80 | 81 |
    • 82 | 83 |
    • 84 | Bioguide ID: 85 | 86 | 87 | 88 | 89 | 93 |
    • 94 | 95 |
    • 96 | OpenCivicData ID: 97 | 98 | 99 | 100 | 101 | 105 |
    • 106 | 107 |
    • 108 | 109 |
    • 110 |
    111 | 112 |
    113 | Status Key:
    • 1 = Active, showing tweets
    • 2 = Collecting tweets, not showing
    • 3 = Not collecting or showing tweets
    • 4 = not collecting, but showing historical tweets
    114 | 115 | <%= render 'identifier_suggestion_templates' %> 116 | 117 | 118 | 143 | -------------------------------------------------------------------------------- /app/views/admin/reports/annual.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 | 6 |
    7 |

    8 | 9 | In <%= @year %> we observed <%= number_with_delimiter @tweet_tally %> tweets from 10 | <%= number_with_delimiter @observed_account_tally %> accounts. 11 | 12 | Of those accounts, <%= number_with_delimiter @deleting_account_tally %> accounts 13 | deleted <%= number_with_delimiter @delete_tally %> tweets. Politwoops moderators 14 | approved <%= number_with_delimiter @approval_tally %> 15 | (<%= number_with_delimiter @approval_pct %>%) twoops from 16 | <%= number_with_delimiter @twooping_account_tally %> accounts. 17 | 18 | (some deletions could be of tweets from prior years) 19 |

    20 | 21 |
    22 | 23 |
    24 |

    25 | Not all accounts tweet with the same tenacity. These are the accounts with the most tweets: 26 |

    27 | 28 | <% if @top_tweeters %> 29 |
      30 | <% @top_tweeters.each do |pol, cnt| %> 31 |
    1. 32 | <%= number_with_delimiter cnt %> 33 | 34 | <%= pol.full_name %> 35 |
    2. 36 | <% end %> 37 |
    38 | <% else %> 39 |

    No tweeters! What is this world coming to?

    40 | <% end %> 41 |
    42 | 43 |
    44 |

    45 | Some accounts delete more tweets than others: 46 |

    47 | <% if @top_deleters %> 48 |
      49 | <% @top_deleters.each do |pol, cnt| %> 50 |
    1. 51 | <%= number_with_delimiter cnt %> 52 | 53 | <%= pol.full_name %> 54 |
    2. 55 | <% end %> 56 |
    57 | <% else %> 58 |

    No tweet deleters! Are we even on earth?

    59 | <% end %> 60 |
    61 | 62 | 63 |
    64 |

    65 | Some accounts have more of their deletions approved than others: 66 |

    67 | <% if @top_twoopers %> 68 |
      69 | <% @top_twoopers.each do |pol, cnt| %> 70 |
    1. 71 | <%= number_with_delimiter cnt %> 72 | 73 | <%= pol.full_name %> 74 |
    2. 75 | <% end %> 76 |
    77 | <% else %> 78 |

    No approved deletes! Momma? Is that you?

    79 | <% end %> 80 |
    81 | 82 |
    83 |

    84 | These are the accounts whose tweets are most likely to become twoops (with a minimum of 5 twoops): 85 |

    86 | 87 | <% if @top_twoopsters %> 88 |
      89 | <% @top_twoopsters.each do |pol, pct| %> 90 |
    1. 91 | <%= pct %>% (of <%= number_with_delimiter pol.tweets.in_year(@year).count %>) 92 | 93 | <%= pol.full_name %> 94 |
    2. 95 | <% end %> 96 |
    97 | <% end %> 98 |
    99 | 100 |
    101 |

    102 | How much does each party tweet? 103 |

    104 | <% if @tweets_per_party %> 105 | 106 | 107 | 108 | 109 | 110 | <% @parties.each do |party| %> 111 | <% if @tweets_per_party[party.id] %> 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | <% end %> 121 | <% end %> 122 | 123 |
    PartyAccountsTweetsDeletesTwoopsMean Twoops
    <%= party.display_name %><%= (number_with_delimiter @tweeting_accounts_per_party[party.id]) || 0 %><%= (number_with_delimiter @tweets_per_party[party.id]) || 0 %><%= (number_with_delimiter @deletes_per_party[party.id]) || 0 %><%= (number_with_delimiter @twoops_per_party[party.id]) || 0 %><%= number_with_precision (@twoops_per_party[party.id] || 0).to_f / @tweeting_accounts_per_party[party.id].to_f, :precision => 1 %>
    124 | <% else %> 125 |

    No party data make your twoopserver cry :(

    126 | <% end %> 127 |
    128 | 129 |
      130 |
    • * Twoops are tweets that are deleted and then approved by moderators.
    • 131 |
    132 |
    133 | 134 | 135 | -------------------------------------------------------------------------------- /app/views/admin/system/status.html.erb: -------------------------------------------------------------------------------- 1 | <% flash.each do |k,v| %> 2 |
    3 | <%= v %> 4 |
    5 | <% end %> 6 | 7 | <% link_to("Refresh", :action => 'status', :controller => 'admin/system') %> 8 | 9 |
    10 | <% if @last_tweet %> 11 | Last tweet seen <%= time_ago_in_words( @last_tweet.created ) %> ago. 12 | <% else %> 13 | No tweets seen yet. 14 | <% end %> 15 |
    16 | 17 |
      18 | <% @worker_statuses.each do |status| %> 19 |
    • 20 |
      23 | <%= csrf_meta_tag %> 24 | 25 | 26 | <% if status[:status] == "running" %> 27 | 28 | <% elsif status[:status] == "restarting" %> 29 | 30 | <% else %> 31 | 32 | <% end %> 33 | <%= status[:worker] %> 34 | <% if status[:last_seen] == 'stale' %> 35 | (last seen @ <%= status[:last_seen] %>) 36 | <% end %> 37 |
      38 |
    • 39 | <% end %> 40 |
    41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | <% @queue_stats.each do |stats| %> 51 | 52 | 53 | 54 | 55 | <% end %> 56 | 57 |
    QueuePending Jobs
    <%= stats[0] %><%= stats[1].nil? ? 0 : stats[1]['current-jobs-ready'] %>
    58 | -------------------------------------------------------------------------------- /app/views/admin/tweets/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :feeds do %> 2 | <%= auto_discovery_link_tag :rss, current_admin_rss %> 3 | <% end %> 4 | 5 | <% if flash[:review_message] %> 6 |
    7 | <%= flash[:review_message] %> 8 |
    9 | <% end %> 10 | 11 |
    12 |
    13 | <% if latest_tweet %> 14 | Last tweet found <%= latest_tweet.modified.strftime "%B %d, %Y at %I:%M %p" %> 15 | <% else %> 16 | No tweets yet! 17 | <% end %> 18 |
    19 | 20 | <% if latest_deleted_tweet %> 21 | Last deleted tweet found <%= latest_deleted_tweet.modified.strftime "%B %d, %Y at %I:%M %p" %> 22 | <% else %> 23 | No deleted tweets yet! 24 | <% end %> 25 |
    26 | Moderation guidelines 27 |
    28 | 29 | 40 |
    41 | 42 | <%= render :partial => "tweets/tweet", :collection => @tweets, :as => :tweet, :locals => {:admin => true} %> 43 | 44 |
    45 | <%= will_paginate @tweets, :next_label => t(:next, :scope => [:politwoops]), :prev_label => t(:previous, :scope => [:politwoops]) %> 46 |
    47 | 48 | 58 | -------------------------------------------------------------------------------- /app/views/errors/not_found.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :head do %> 2 | <%= stylesheet_link_tag "broken" %> 3 | <% end %> 4 | 5 | <% content_for :body_class do %> class="error404" <% end %> 6 | 7 | 8 | 13 | 14 |
    15 |

    404 error

    16 |

    Twoopsie Daisies!

    17 |

    Looks like you've landed on a page that no longer exists

    18 |

    Go back to the home page and try again.

    19 |
    20 | -------------------------------------------------------------------------------- /app/views/errors/status_5xx.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title do %> 2 | Temporarily Down 3 | <% end %> 4 | 5 | <% content_for :body_class do %> class="error404" <% end %> 6 | 7 | <% content_for :head do %> 8 | <%= stylesheet_link_tag "broken" %> 9 | <% end %> 10 | 11 | 16 | 17 |

    You Broke Politwoops!

    18 | 19 |

    We sure can't delete this error page, but we're working on fixing the problem right now. Check back soon!

    20 | 21 | -------------------------------------------------------------------------------- /app/views/layouts/_analytics.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 36 | -------------------------------------------------------------------------------- /app/views/layouts/admin.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Politwoops - <%= t :slogan, :scope => [:politwoops] %> 5 | 6 | <%= JavascriptExports %> 7 | 8 | <%= stylesheet_link_tag "style" %> 9 | <%= stylesheet_link_tag "admin" %> 10 | 11 | <%= javascript_include_tag "jquery-1.7.1.min" %> 12 | <%= javascript_include_tag "jquery.placehold-0.3.min" %> 13 | 14 | <%= csrf_meta_tag %> 15 | 16 | 17 | 18 | 19 | <%= content_for :feeds %> 20 | 21 | <%= yield(:body_class) %> <% end %>> 22 |
    23 |
    24 |
    25 | 40 |

    Politwoops Deleted Tweets from Politicians

    41 | 45 |
    46 | 47 |
    48 |
    49 | 50 |
    51 | 52 |

    Politwoops Admin

    53 | 61 |
    62 |
    63 | 64 |
    65 | <%= yield %> 66 |
    67 | 68 |
    69 |
    70 | 88 | 89 | <%= javascript_include_tag "jquery.tmpl.min.js" %> 90 | <%= javascript_include_tag "congressapi" %> 91 | <%= javascript_include_tag "opencivicdataapi" %> 92 | <%= javascript_include_tag "politwoops" %> 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /app/views/politicians/all.html.erb: -------------------------------------------------------------------------------- 1 |

    2 | Politicians We Follow 3 |

    4 | 5 | <%= render :template => "shared/filterform" %> 6 | 7 |

    This may be an incomplete list. If you think we're missing someone, please email us with their Name, State, Political Party, Office they hold or are seeking and, of course, Twitter handle. Thanks!

    8 |
    9 | 10 | 11 | 12 | <% @politicians.each do |pol|%> 13 | 14 | 22 | 32 | 37 | 42 | 43 | 48 | 49 | 55 | 56 | <% end %> 57 | 58 |
    15 | <%= pol.user_name %> 21 | 23 | 24 | 25 | <%= pol.first_name %> 26 | <%= pol.middle_name %> 27 | <%= pol.last_name %> 28 | <%= pol.suffix %> 29 | 30 | 31 | 33 | 34 | <% if pol.state %> <%= pol.state.upcase %> <% end %> 35 | 36 | 38 | 39 | <% if pol.office %><%= pol.office.title %> <% end %> 40 | 41 | 50 | 51 | <% if pol.party %> <%= pol.party.display_name %> <% end %> 52 | <% if pol.status == 4 %>inactive <% end %> 53 | 54 |
    59 | 60 | 61 | <%= render :template => "shared/pager" %> 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/views/politicians/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title do %> 2 | Deleted tweets for <%= office_title_for(@politician) %> <%= @politician.first_name %> <%=@politician.last_name %> (<%= party_name_for(@politician) %>)<% if @page > 1 %> (Page <%= @page %>)<% end %> 3 | <% end %> 4 | 5 |

    Politwoops

    6 |

    Deleted Tweets from Politicians

    7 | 8 | 9 |

    Politwoops tracks deleted tweets by current politicians, including those in office and candidates. Explore the collection of tweets that they didn't want you to see. If you think we're missing someone, please email us with their Name, State, Political Party, Office they hold or are seeking and, of course, Twitter handle.

    10 | 11 |
    12 |

    Deleted Tweets from <%= office_abbr_for(@politician) %> <%= @politician.first_name %> <%=@politician.last_name %> (<%= party_name_for(@politician) %>)

    13 | 23 |

    24 | <% @accounts.each do |acc| %> 25 | 30 | <% end %> 31 |
    32 |
    33 |
    34 | <%= render :partial => "tweets/tweet", :collection => @tweet_map['all'], :object => :tweet %> 35 |
    36 | 37 | <% @accounts.each do |acc| %> 38 | 43 | <% end %> 44 | 45 | 60 | 61 | <%= render :template => "shared/pager" %> 62 | 63 | <%= render :template => "shared/screenshotbox" %> 64 | -------------------------------------------------------------------------------- /app/views/shared/filterform.html.erb: -------------------------------------------------------------------------------- 1 | 40 | -------------------------------------------------------------------------------- /app/views/shared/pager.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= will_paginate @tweets, :next_label => t(:next, :scope => [:politwoops]), :prev_label => t(:previous, :scope => [:politwoops]) %> 3 | 4 |
    5 |

    Results per page

    6 | 13 |
    14 | 15 |
    16 | 17 | -------------------------------------------------------------------------------- /app/views/shared/screenshotbox.html.erb: -------------------------------------------------------------------------------- 1 | 9 | 11 |
    12 |
    13 | 14 | -------------------------------------------------------------------------------- /app/views/tweets/_tweet.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | <% if flash[tweet.id] %> 5 |
    6 | <%= flash[tweet.id] %> 7 |
    8 | <% end %> 9 | 10 |
    11 | 12 | <%= tweet.politician.user_name %> 18 | 19 |
    22 |
    23 |
    24 | 25 |
    26 |
    27 |

    28 | <%= tweet.details['user']['name'] %> (<%= tweet.politician.party.name.upcase %>) 29 | @<%= tweet.politician.user_name %> 30 |

    31 | <% if tweet.politician.status == 4 %> 32 |
    Politwoops no longer follows this account.
    33 | <% end %> 34 | 35 | <%= format_tweet(tweet).to_s.html_safe %> 36 |
    37 | 38 | 41 | 42 |
    43 | <% if tweet.tweet_images.any? %> 44 |

    Screenshots of links in this tweet

    45 | <% tweet.tweet_images.each do |image| %> 46 | 47 | 52 | 53 | <% end %> 54 | <% end %> 55 |
    56 | 57 | 58 |
    59 | 60 | <% if !(defined?(admin) and admin) %> 61 | 65 | <% end %> 66 | 67 |
    68 | 69 | <% if defined?(admin) and admin %> 70 |
    71 | <%= form_tag admin_review_tweet_path(tweet) do %> 72 | <%= hidden_field_tag "return_to", "#{request.url}##{tweet.id}" %> 73 | 74 | <% if tweet.reviewed? %> 75 |
    76 | <% if tweet.approved? %> 77 | <%= submit_tag "Unapprove" %> 78 | <% else %> 79 | <%= submit_tag "Approve" %> 80 | <% end %> 81 | 82 | <% if tweet.reviewed_at %> 83 | 84 | Reviewed on <%= tweet.reviewed_at.strftime "%B %d, %Y at %I:%M %p" %>: 85 | 86 | <% end %> 87 | 88 |
    89 | <% if tweet.review_message.present? %> 90 | <%= light_format tweet.review_message %> 91 | <% else %> 92 | (No message entered.) 93 | <% end %> 94 |
    95 | Edit message 96 |
    97 | 98 | 103 | 104 | <% else %> 105 | <%= submit_tag "Unapprove" %> 106 | <%= submit_tag "Approve" %> 107 |
    108 | 111 | 112 | <% end %> 113 | <% end %> 114 |
    115 | <% end %> 116 | 117 |
    118 | -------------------------------------------------------------------------------- /app/views/tweets/index.html.erb: -------------------------------------------------------------------------------- 1 |

    Politwoops

    2 |

    Deleted Tweets from Politicians

    3 | 4 | 5 |

    Politwoops tracks deleted tweets by current politicians, including those in office and candidates. Explore the collection of tweets that they didn't want you to see. If you think we're missing someone, please email us with their Name, State, Political Party, Office they hold or are seeking and, of course, Twitter handle.

    6 | 7 | 10 | 11 | <% content_for :feeds do %> 12 | <% if @politician %> 13 | <%= auto_discovery_link_tag :rss, politician_url(@politician.user_name, :format => :rss) %> 14 | <% elsif @query %> 15 | <%= auto_discovery_link_tag :rss, index_url(:format => :rss, :q => @query) %> 16 | <% else %> 17 | <%= auto_discovery_link_tag :rss, index_url(:format => :rss) %> 18 | <% end %> 19 | <% end %> 20 | 21 | <% if @tweets.empty? %> 22 | No results for the filter options you selected. 23 | <% else %> 24 | <%= render :partial => "tweets/tweet", :collection => @tweets, :object => :tweet %> 25 | 26 | <%= render :template => "shared/pager" %> 27 | 28 | <%= render :template => "shared/screenshotbox" %> 29 | <% end %> 30 | -------------------------------------------------------------------------------- /app/views/tweets/index.rss.builder: -------------------------------------------------------------------------------- 1 | xml.rss "version" => "2.0" do 2 | xml.channel do 3 | if @politician 4 | xml.title "Politwoops - Tweets by @#{@politician.user_name}" 5 | xml.link politician_url(@politician.user_name) 6 | elsif @query 7 | xml.title "Politwoops - Tweets matching \"#{@query}\"" 8 | xml.link root_url(:q => @query) 9 | elsif @admin 10 | xml.title "Politwoops - Unreviewed tweets" 11 | xml.link root_url 12 | else 13 | xml.title "Politwoops" 14 | xml.link root_url 15 | end 16 | 17 | xml.description t(:slogan, :scope => :politwoops) 18 | 19 | @tweets.each do |tweet| 20 | xml.item do 21 | xml.title "@#{tweet.user_name} -- #{byline tweet, false}" 22 | xml.description tweet.content 23 | xml.link tweet_url(tweet) 24 | xml.pubDate rss_date(tweet.modified) 25 | end 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /app/views/tweets/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title do %> 2 | Deleted tweet from <%= @tweet.details['user']['name'] %> 3 | <% end %> 4 | 5 |

    Politwoops

    6 |

    Deleted Tweets from Politicians

    7 | 8 | 9 | <%= render :partial => "tweets/tweet", :locals => {:tweet => @tweet} %> 10 | -------------------------------------------------------------------------------- /bin/generate_sitemap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source $HOME/unicorn_environment.sh 4 | 5 | cd /projects/twoops/www/current && bundle exec rake sitemap:refresh:no_ping > /dev/null 6 | -------------------------------------------------------------------------------- /bin/logrotate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | kill -USR1 $(ps auxwww | grep twoops | grep unicorn | grep master| awk '{print $2}') 4 | -------------------------------------------------------------------------------- /bin/reset_avatars.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PATH=/projects/twoops/.gem/ruby/1.8/bin:$PATH 4 | export GEM_HOME=/projects/twoops/.gem/ruby/1.8 5 | 6 | source $HOME/unicorn_environment.sh 7 | 8 | cd /projects/twoops/www/current && bundle exec rake politicians:reset_avatars where_blank=1 9 | -------------------------------------------------------------------------------- /bin/run-collector: -------------------------------------------------------------------------------- 1 | nohup run-tweets-client -v -i > tweets-client.out & 2 | nohup run-politwoops-worker -v -i > politwoops-worker.out & 3 | nohup run-screenshot-worker -v -i > screenshot-worker.out & 4 | -------------------------------------------------------------------------------- /bin/run-collector-dockercmd: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | /usr/bin/beanstalkd -f 60000 -b /data & 3 | ./run-tweets-client -v -i & 4 | ./run-politwoops-worker -v -i & 5 | ./run-screenshot-worker -v -i & 6 | wait 7 | -------------------------------------------------------------------------------- /bin/run-politwoops-worker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $HOME/politwoops-tweet-collector 4 | export PYTHONPATH=$PYTHONPATH:`pwd`/lib 5 | $HOME/virt/bin/python ./bin/politwoops-worker.py --loglevel=notice --output=../log/politwoops-worker.log --images --restart 6 | -------------------------------------------------------------------------------- /bin/run-screenshot-worker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $HOME/politwoops-tweet-collector 4 | export PYTHONPATH=$PYTHONPATH:`pwd`/lib 5 | export PATH="$HOME/phantomjs-1.9.7-linux-x86_64/bin":"$PATH" 6 | $HOME/virt/bin/python ./bin/screenshot-worker.py --loglevel=notice --output=../log/screenshot-worker.log --restart 7 | -------------------------------------------------------------------------------- /bin/run-tweets-client: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $HOME/politwoops-tweet-collector 4 | export PYTHONPATH=$PYTHONPATH:`pwd`/lib 5 | $HOME/virt/bin/python ./bin/tweets-client.py --loglevel=notice --output=../log/tweets-client.log --restart 6 | -------------------------------------------------------------------------------- /bin/twoops-tweets-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /projects/twoops/politwoops-twitter-client 4 | nohup ./bin/tweets-client.py -v > tweets-client.out 5 | -------------------------------------------------------------------------------- /bin/twoops-workers-start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # export LD_LIBRARY_PATH=/projects/twoops/phantomjs/lib:$LD_LIBRARY_PATH 4 | 5 | cd /projects/twoops/politwoops-twitter-client 6 | nohup ./bin/politwoops-worker.py -v -i > politwoops-worker.out 7 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | map Politwoops::Application.config.try(:propub_url_root) || "/" do 5 | run Politwoops::Application 6 | end 7 | -------------------------------------------------------------------------------- /config/admin.yml.example: -------------------------------------------------------------------------------- 1 | :username: 2 | :password: 3 | 4 | # used for obfuscating the RSS URL 5 | :rss_secret: 6 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # If you have a Gemfile, require the gems listed there, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(:default, Rails.env) if defined?(Bundler) 8 | 9 | module Politwoops 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Custom directories with classes and modules you want to be autoloadable. 16 | config.autoload_paths += %W(lib).flat_map{ |pth| Dir[File.join(config.root, pth, '**/')] } 17 | require 'core_ext' 18 | 19 | # Only load the plugins named here, in the order given (default is alphabetical). 20 | # :all can be used as a placeholder for all plugins not explicitly named. 21 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 22 | 23 | # Activate observers that should always be running. 24 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 25 | 26 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 27 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 28 | # config.time_zone = 'Central Time (US & Canada)' 29 | config.time_zone = "Eastern Time (US & Canada)" 30 | 31 | config.middleware.use Rack::Deflater 32 | 33 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 34 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 35 | # config.i18n.default_locale = :de 36 | 37 | # JavaScript files you want as :defaults (application.js is always included). 38 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) 39 | 40 | # Configure the default encoding used in templates for Ruby 1.9. 41 | config.encoding = "utf-8" 42 | 43 | # Configure sensitive parameters which will be filtered from the log file. 44 | config.filter_parameters += [:password, :password_confirmation] 45 | config.exceptions_app = self.routes 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | gemfile = File.expand_path('../../Gemfile', __FILE__) 5 | begin 6 | ENV['BUNDLE_GEMFILE'] = gemfile 7 | require 'bundler' 8 | Bundler.setup 9 | rescue Bundler::GemNotFound => e 10 | STDERR.puts e.message 11 | STDERR.puts "Try running `bundle install`." 12 | exit! 13 | end if File.exist?(gemfile) 14 | -------------------------------------------------------------------------------- /config/config.yml.example: -------------------------------------------------------------------------------- 1 | :sitemap_default_url: 'http://host.com' 2 | 3 | :admin: 4 | :username: 5 | :password: 6 | 7 | # used for obfuscating the RSS URL 8 | :rss_secret: 9 | 10 | # used to sign session cookies, pick anything long and secure 11 | :session_secret: 12 | 13 | :twitter: 14 | :consumer_key: 15 | :consumer_secret: 16 | :access_token: 17 | :access_token_secret: 18 | 19 | # These settings should mirror those set in the twitter 20 | # client configuration. 21 | :heartbeat_interval: 30 22 | :heartbeats_directory: /otherwise/empty/directory/ 23 | :heartbeats_expected: 24 | - tweets-client.py 25 | - politwoops-worker.py 26 | - screenshot-worker.py 27 | # Monitoring hosts are allowed to query /admin/status.json 28 | # without authentication. 29 | :monitoring_hosts: 30 | - localhost 31 | -------------------------------------------------------------------------------- /config/database.yml.example: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: mysql2 3 | database: rails_dev 4 | username: dev 5 | password: devpwd 6 | 7 | test: 8 | adapter: mysql2 9 | database: rails_test 10 | username: test 11 | password: testpwd 12 | 13 | production: 14 | adapter: mysql2 15 | database: rails_prod 16 | username: prod 17 | password: prodpwd 18 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Politwoops::Application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Politwoops::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the webserver when you make code changes. 7 | config.cache_classes = false 8 | 9 | config.eager_load = false 10 | 11 | # Log error messages when you accidentally call methods on nil. 12 | config.whiny_nils = true 13 | 14 | # Show full error reports and disable caching 15 | config.consider_all_requests_local = true 16 | config.action_controller.perform_caching = false 17 | 18 | # Don't care if the mailer can't send 19 | config.action_mailer.raise_delivery_errors = false 20 | 21 | # Print deprecation notices to the Rails logger 22 | config.active_support.deprecation = :log 23 | 24 | # Only use best-standards-support built into browsers 25 | config.action_dispatch.best_standards_support = :builtin 26 | 27 | config.paperclip_defaults = { 28 | :storage => :filesystem, 29 | :path => "/:attachment/:filename", 30 | :interpolations => { 31 | :base_path => "#{Rails.root}/public/images" 32 | } 33 | } 34 | end 35 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Politwoops::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The production environment is meant for finished, "live" apps. 5 | # Code is not reloaded between requests 6 | config.eager_load = true 7 | config.cache_classes = true 8 | 9 | # Full error reports are disabled and caching is turned on 10 | config.consider_all_requests_local = false 11 | config.action_controller.perform_caching = true 12 | 13 | config.log_level = :info 14 | 15 | # Specifies the header that your server uses for sending files 16 | config.action_dispatch.x_sendfile_header = "X-Sendfile" 17 | 18 | # For nginx: 19 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' 20 | 21 | # If you have no front-end server that supports something like X-Sendfile, 22 | # just comment this out and Rails will serve the files 23 | 24 | # See everything in the log (default is :info) 25 | # config.log_level = :debug 26 | 27 | # Use a different logger for distributed setups 28 | # config.logger = SyslogLogger.new 29 | 30 | # Use a different cache store in production 31 | config.cache_store = :mem_cache_store, 32 | "newsapps-vpc.o2zdh0.0001.use1.cache.amazonaws.com", 33 | { :namespace => 'politwoops', compress: true } 34 | #{ :namespace => 'politwoops', expires_in: 1.day, compress: true } 35 | 36 | # Disable Rails's static asset server 37 | # In production, Apache or nginx will already do this 38 | config.serve_static_files = false 39 | 40 | # Enable serving of images, stylesheets, and javascripts from an asset server 41 | # config.action_controller.asset_host = "http://assets.example.com" 42 | 43 | # Disable delivery errors, bad email addresses will be ignored 44 | # config.action_mailer.raise_delivery_errors = false 45 | 46 | # Enable threaded mode 47 | # config.threadsafe! 48 | 49 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 50 | # the I18n.default_locale when a translation can not be found) 51 | config.i18n.fallbacks = true 52 | 53 | # Send deprecation notices to registered listeners 54 | config.active_support.deprecation = :notify 55 | 56 | config.logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT)) 57 | config.propub_url_root = "/politwoops" 58 | config.assets.compress = true 59 | config.assets.compile = true 60 | config.assets.digest = true 61 | 62 | config.paperclip_defaults = { 63 | :storage => :s3, 64 | :s3_permissions => :public_read, 65 | :path => "/:attachment/:filename", 66 | :url => ":s3_path_url", 67 | :bucket => ENV['S3_BUCKET_NAME'], 68 | :s3_credentials => { 69 | :access_key_id => ENV['AWS_ACCESS_KEY_ID'], 70 | :secret_access_key => ENV['AWS_SECRET_ACCESS_KEY'] 71 | }, 72 | :interpolations => { 73 | :base_path => "/images" 74 | } 75 | } 76 | 77 | end 78 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Politwoops::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.eager_load = false 9 | 10 | config.cache_classes = true 11 | 12 | # Log error messages when you accidentally call methods on nil. 13 | config.whiny_nils = true 14 | 15 | # Show full error reports and disable caching 16 | config.consider_all_requests_local = true 17 | config.action_controller.perform_caching = false 18 | 19 | # Raise exceptions instead of rendering exception templates 20 | config.action_dispatch.show_exceptions = false 21 | 22 | # Disable request forgery protection in test environment 23 | config.action_controller.allow_forgery_protection = false 24 | 25 | # Tell Action Mailer not to deliver emails to the real world. 26 | # The :test delivery method accumulates sent emails in the 27 | # ActionMailer::Base.deliveries array. 28 | config.action_mailer.delivery_method = :test 29 | 30 | # Use SQL instead of Active Record's schema dumper when creating the test database. 31 | # This is necessary if your schema can't be completely dumped by the schema dumper, 32 | # like if you have constraints or database-specific column types 33 | # config.active_record.schema_format = :sql 34 | 35 | # Print deprecation notices to the stderr 36 | config.active_support.deprecation = :stderr 37 | end 38 | -------------------------------------------------------------------------------- /config/initializers/90_javascript_exports.rb: -------------------------------------------------------------------------------- 1 | cfg = YAML.load_file "#{Rails.root}/config/config.yml" 2 | JavascriptExports.export :sunlight_api_key, cfg[:sunlight_api_key] 3 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | 13 | # Precompile additional assets. 14 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 15 | Rails.application.config.assets.precompile += %w(propublica_base/base.css propublica_base/master.css propublica_base/print.css propublica_base/woland.css) 16 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/global_settings.rb: -------------------------------------------------------------------------------- 1 | Settings = YAML.load_file "#{Rails.root}/config/config.yml" 2 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /config/initializers/paperclip.rb: -------------------------------------------------------------------------------- 1 | if Rails.configuration.respond_to?("paperclip_defaults") 2 | Paperclip::Attachment.default_options.merge!(Rails.configuration.paperclip_defaults) 3 | interpolations = Paperclip::Attachment.default_options.fetch(:interpolations, {}) 4 | interpolations.each do |k, v| 5 | Paperclip.interpolates k do |attachment, style| 6 | v 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | 8 | config = YAML.load_file "#{Rails.root}/config/config.yml" 9 | Politwoops::Application.config.secret_token = config[:session_secret] -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Politwoops::Application.config.session_store :cookie_store, :key => '_politwoops_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Politwoops::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /config/initializers/twitter.rb: -------------------------------------------------------------------------------- 1 | cfg = YAML.load_file "#{Rails.root}/config/config.yml" 2 | $twitter = Twitter::REST::Client.new do |config| 3 | config.consumer_key = cfg[:twitter][:consumer_key] 4 | config.consumer_secret = cfg[:twitter][:consumer_secret] 5 | config.access_token = cfg[:twitter][:oauth_token] 6 | config.access_token_secret = cfg[:twitter][:oauth_token_secret] 7 | end 8 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | politwoops: 6 | slogan: "Deleted tweets from politicians" 7 | login: "login" 8 | logout: "logout" 9 | menu: "menu" 10 | hdo_project: "A hackdeoverheid project" 11 | show: "Show" 12 | edit: "Edit" 13 | destroy: "Destroy" 14 | sure: "Are you sure?" 15 | ago: "ago" 16 | next: "Next" 17 | previous: "Previous" 18 | search: "Search" 19 | add: "Add" 20 | yes_label: "Yes" 21 | no_label: "No" 22 | 23 | groups: 24 | errors_explanation: 25 | one: "1 error prohibited saving this group:" 26 | other: "%{count} errors prohibited saving this group:" 27 | name_label: "Name" 28 | name_explanation: "A short name, without spaces" 29 | description_label: "Description" 30 | description_explanation: "A description of the group" 31 | politicians_label: "Politicians" 32 | politicians_explanation: "Mark politicians to add or remove them from the group." 33 | sponsor_label: "Sponsor" 34 | sponsor_explanation: "Who is sponsoring this group? (if any)" 35 | flag_label: "Country" 36 | base_url_label: "Base URL" 37 | domain_label: "Is a domain" 38 | add_heading: "Add a group" 39 | edit_heading: "Edit group" 40 | view_heading: "View group" 41 | index_heading: "Groups" 42 | model_name: "Group" 43 | success_create: "The group was successfully created" 44 | success_update: "The group was successfully updated" 45 | 46 | countries: 47 | index_heading: "International sites" 48 | 49 | groups_politicians: 50 | search_label: "Search for twitter user" 51 | success_update: "The politician was successfully added to the group" 52 | no_users_found: "Sorry, we did not find anything. Try again." 53 | 54 | pages: 55 | errors_explanation: 56 | one: "1 error prohibited saving this page:" 57 | other: "%{count} errors prohibited saving this page:" 58 | title_label: "Title" 59 | slug_label: "Slug" 60 | content_label: "Contents" 61 | language_label: "Language" 62 | add_heading: "Add a page" 63 | edit_heading: "Edit page" 64 | index_heading: "Pages" 65 | model_name: "Page" 66 | success_create: "The page was successfully created" 67 | success_update: "The page was successfully updated" 68 | 69 | parties: 70 | errors_explanation: 71 | one: "1 error prohibited saving this political party:" 72 | other: "%{count} errors prohibited saving this political party:" 73 | name_label: "Name" 74 | name_explanation: "A short name, without spaces" 75 | add_heading: "Add a political party" 76 | edit_heading: "Edit political party" 77 | index_heading: "Political parties" 78 | model_name: "Political party" 79 | success_create: "The political party was successfully created" 80 | success_update: "The political party was successfully updated" 81 | 82 | politicians: 83 | errors_explanation: 84 | one: "1 error prohibited saving this politician:" 85 | other: "%{count} errors prohibited saving this politician:" 86 | name_label: "Twitter name" 87 | name_explanation: "The twitter user might already be in our system. Click 'Edit group' to add the user there." 88 | status_label: "Is active" 89 | add_heading: "Add a Politician" 90 | edit_heading: "Edit politician" 91 | index_heading: "Politicians" 92 | model_name: "Politician" 93 | success_create: "The politician was successfully created" 94 | success_update: "The politician was successfully updated" 95 | 96 | statistics: 97 | tweets: "Tweets" 98 | deleted_tweets: "Deleted tweets" 99 | percentage_deleted_tweets: "Percentage deleted tweets" 100 | politicians: "Number of politicians" 101 | today: "today" 102 | index_heading: "Statistics" 103 | 104 | tweets: 105 | byline: "Deleted %{delay} %{when}, via %{what}. %{retweet}" 106 | byline_text: "Deleted %{delay} %{when}" 107 | reply: "reply" 108 | retweet: "retweet" 109 | 110 | user_sessions: 111 | login_label: "login" 112 | password_label: "password" 113 | remember_me_label: "remember me" 114 | login_action_label: "Login" 115 | success_create: "You're in! You can go to the menu now." 116 | success_destroy: "You were successfully logged out!" 117 | 118 | users: 119 | errors_explanation: 120 | one: "1 error prohibited saving this user:" 121 | other: "%{count} errors prohibited saving this user:" 122 | login_label: "Login" 123 | email_label: "Email address" 124 | password_label: "Password" 125 | password_change_label: "Password" 126 | password_confirmation_label: "Password (x2)" 127 | add_heading: "Register new user" 128 | edit_heading: "Edit user" 129 | index_heading: "Users" 130 | menu_heading: "Menu" 131 | model_name: "User" 132 | require_user: "You must be logged in to view this page" 133 | require_admin_user: "You do not have sufficient rights to view this page" 134 | require_no_user: "You must be logged out to view this page" 135 | success_create: "The user was successfully created" 136 | success_update: "The user was successfully updated" 137 | 138 | twitter: 139 | retweet_notice: "Look! @%{politician} deleted a tweet! %{tweet_url}" 140 | 141 | #activerecord: 142 | #errors: 143 | # template: 144 | # header: 145 | # one: "1 fout zorgde er voor dat de %{model} niet opgeslagen kon worden" 146 | # other: "%{count} fouten zorgden er voor dat de %{model} niet opgeslagen kon worden" 147 | # body: "Er waren problemen met de volgende velden:" 148 | 149 | authlogic: 150 | error_messages: 151 | login_blank: can not be blank 152 | login_not_found: is not valid 153 | login_invalid: "should use only letters, numbers, spaces, and .-_@ please." 154 | consecutive_failed_logins_limit_exceeded: "Consecutive failed logins limit exceeded, account is disabled." 155 | email_invalid: should look like an email address. 156 | password_blank: can not be blank 157 | password_invalid: is not valid 158 | not_active: Your account is not active 159 | not_confirmed: Your account is not confirmed 160 | not_approved: Your account is not approved 161 | no_authentication_details: You did not provide any details for authentication. 162 | models: 163 | user_session: UserSession 164 | attributes: 165 | user_session: 166 | login: login 167 | email: email 168 | password: password 169 | remember_me: remember me 170 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Politwoops::Application.routes.draw do 2 | 3 | root :to => "tweets#index" 4 | 5 | get "index(.:format)" => "tweets#index", :as => :index 6 | get "tweet/:id" => "tweets#show", :as => :tweet 7 | get "tweet/:tweet_id/thumb/:basename.:format" => "tweets#thumbnail" 8 | get "user/:user_name" => "politicians#show", :as => :politician 9 | get "users/" => "politicians#all", :as => :all_politicians 10 | get "party/:name" => "parties#show", :as => :party 11 | 12 | namespace :admin do 13 | get "status" => "system#status" 14 | get "restart" => "system#restart" 15 | get "report" => "system#report" 16 | 17 | get "review" => "tweets#index", :reviewed => false, :approved => false, :as => "review" 18 | get "unapproved" => "tweets#index", :reviewed => true, :approved => false, :as => "unapproved" 19 | get "approved" => "tweets#index", :reviewed => true, :approved => true, :as => "approved" 20 | 21 | get "users" => "politicians#admin_list", :as => "admin_list" 22 | get "user/:id" => "politicians#admin_user", :as => "admin_user" 23 | post "user/:id/save" => "politicians#save_user", :as => "save_user" 24 | get "users/new" => "politicians#new_user", :as => "new_user" 25 | get "users/get-twitter-id/:screen_name" => "politicians#get_twitter_id", :as => "get_twitter_id" 26 | 27 | get "offices" => "offices#list", :as => "list_offices" 28 | get "offices/add" => "offices#add", :as => "add_office" 29 | post "offices/save" => "offices#save", :as => "save_office" 30 | 31 | get "offices/:id" => "offices#edit", :as => "edit_office" 32 | get "review/:rss_secret.rss" => "tweets#index", :reviewed => false, :approved => false, :as => "review_rss", :format => "rss" 33 | 34 | post "review/:id" => "tweets#review", :via => [:post], :as => "review_tweet" 35 | 36 | get "reports/annual/:year" => "reports#annual", :as => "annual_report_year" 37 | get "reports/annual" => "reports#annual", :as => "annual_report" 38 | root :to => "tweets#index" 39 | end 40 | 41 | get "5xx", :to => "errors#down" 42 | get "404", :to => "errors#not_found" 43 | get "*anything", :to => "errors#not_found" 44 | 45 | end 46 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | development: 2 | secret_token: 'foo' 3 | secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 4 | 5 | production: 6 | secret_token: 'foo' 7 | secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 8 | -------------------------------------------------------------------------------- /config/sitemap.rb: -------------------------------------------------------------------------------- 1 | SitemapGenerator::Sitemap.create(:default_host => Settings[:sitemap_default_url], 2 | :include_root => false) do 3 | most_recent_twoop = DeletedTweet.order(:modified).first 4 | if most_recent_twoop 5 | add '/', 6 | :host => Settings[:sitemap_default_url], 7 | :changefreq => 'hourly', 8 | :priority => 1 9 | else 10 | add '/', 11 | :host => Settings[:sitemap_default_url], 12 | :changefreq => 'hourly', 13 | :lastmod => most_recent_twoop.modified, 14 | :priority => 1 15 | end 16 | 17 | add '/users', 18 | :host => Settings[:sitemap_default_url], 19 | :changefreq => 'weekly', 20 | :priority => 1 21 | 22 | Politician.all.each do |pol| 23 | add url_for(:host => Settings[:sitemap_default_url], 24 | :controller => 'politicians', 25 | :action => 'show', 26 | :user_name => pol.user_name), 27 | :changefreq => 'weekly' 28 | end 29 | 30 | Party.all.each do |party| 31 | add url_for(:host => Settings[:sitemap_default_url], 32 | :controller => 'parties', 33 | :action => 'show', 34 | :name => party.name), 35 | :changefreq => 'daily', 36 | :priority => 0.1 37 | end 38 | 39 | DeletedTweet.find_each do |twoop| 40 | add url_for(:host => Settings[:sitemap_default_url], 41 | :controller => 'tweets', 42 | :action => 'show', 43 | :id => twoop.id), 44 | :lastmod => twoop.modified, 45 | :priority => 0.9 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /config/unicorn.conf.rb: -------------------------------------------------------------------------------- 1 | worker_processes 2 2 | working_directory "/web/" 3 | listen 80 4 | preload_app true 5 | 6 | before_fork do |server, worker| 7 | defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect! 8 | end 9 | 10 | after_fork do |server, worker| 11 | defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20120904211055_add_state_to_politician.rb: -------------------------------------------------------------------------------- 1 | class AddStateToPolitician < ActiveRecord::Migration 2 | def self.up 3 | add_column :politicians, :state, :string 4 | add_column :politicians, :account, :int 5 | add_column :politicians, :office, :int 6 | end 7 | 8 | def self.down 9 | remove_column :politicians, :state 10 | remove_column :politicians, :account 11 | remove_column :politicians, :office 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20120904211225_create_account_type_table.rb: -------------------------------------------------------------------------------- 1 | class CreateAccountTypeTable < ActiveRecord::Migration 2 | def self.up 3 | create_table :account_types do |t| 4 | t.string :type 5 | end 6 | end 7 | 8 | def self.down 9 | drop_table :account_types 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20120904211632_create_office_table.rb: -------------------------------------------------------------------------------- 1 | class CreateOfficeTable < ActiveRecord::Migration 2 | def self.up 3 | create_table :office_held do |t| 4 | t.string :title, :null => false 5 | t.string :abbreviation, :null => false 6 | end 7 | end 8 | 9 | def self.down 10 | drop_table :office_held 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20120905195444_rename_office_table_to_offices.rb: -------------------------------------------------------------------------------- 1 | class RenameOfficeTableToOffices < ActiveRecord::Migration 2 | def self.up 3 | rename_table :office_held, :offices 4 | end 5 | 6 | def self.down 7 | rename_table :offices, :office_held 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20120905220238_change_column_on_account_type.rb: -------------------------------------------------------------------------------- 1 | class ChangeColumnOnAccountType < ActiveRecord::Migration 2 | def up 3 | rename_column :account_types, :type, :name 4 | end 5 | 6 | def down 7 | rename_column :account_types, :name, :type 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20120906150719_change_columns_on_politicians.rb: -------------------------------------------------------------------------------- 1 | class ChangeColumnsOnPoliticians < ActiveRecord::Migration 2 | def up 3 | rename_column :politicians, :office, :office_id 4 | rename_column :politicians, :account, :account_id 5 | end 6 | 7 | def down 8 | rename_column :politicians, :office_id, :office 9 | rename_column :politicians, :account_id, :account 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20120906151117_change_column_on_pol_account.rb: -------------------------------------------------------------------------------- 1 | class ChangeColumnOnPolAccount < ActiveRecord::Migration 2 | def up 3 | rename_column :politicians, :account_id, :account_type_id 4 | end 5 | 6 | def down 7 | rename_column :politicians, :account_type_id, :account_id 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20120906181703_create_account_links.rb: -------------------------------------------------------------------------------- 1 | class CreateAccountLinks < ActiveRecord::Migration 2 | def change 3 | create_table :account_links do |t| 4 | t.integer :politician_id 5 | t.integer :link_id 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20120910213133_add_name_fields_to_politician.rb: -------------------------------------------------------------------------------- 1 | class AddNameFieldsToPolitician < ActiveRecord::Migration 2 | def self.up 3 | add_column :politicians, :first_name, :string 4 | add_column :politicians, :middle_name, :string 5 | add_column :politicians, :last_name, :string 6 | add_column :politicians, :suffix, :string 7 | end 8 | 9 | def self.down 10 | remove_column :politicians, :first_name 11 | remove_column :politicians, :middle_name 12 | remove_column :politicians, :last_name 13 | remove_column :politicians, :suffix 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20120914202322_add_field_to_party.rb: -------------------------------------------------------------------------------- 1 | class AddFieldToParty < ActiveRecord::Migration 2 | def self.up 3 | add_column :parties, :display_name, :string 4 | end 5 | 6 | def self.down 7 | remove_column :parties, :display_name 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20121003160601_add_tweets_modified_index.rb: -------------------------------------------------------------------------------- 1 | class AddTweetsModifiedIndex < ActiveRecord::Migration 2 | def up 3 | add_index :tweets, :modified 4 | add_index :deleted_tweets, :modified 5 | end 6 | 7 | def down 8 | remove_index :tweets, :column => :modified 9 | remove_index :deleted_tweets, :column => :modified 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20121108214001_add_tweet_image_tweet_id_index.rb: -------------------------------------------------------------------------------- 1 | class AddTweetImageTweetIdIndex < ActiveRecord::Migration 2 | def up 3 | add_index :tweet_images, :tweet_id 4 | end 5 | 6 | def down 7 | remove_index :tweet_images, :tweet_id 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20131107162403_add_retweet_fields.rb: -------------------------------------------------------------------------------- 1 | class AddRetweetFields < ActiveRecord::Migration 2 | def change 3 | # Integer with byte-size 8 should equate to "big integer" in mysql 4 | 5 | add_column :tweets, :retweeted_id, :integer, { :limit => 8 } 6 | add_column :tweets, :retweeted_content, :string 7 | add_column :tweets, :retweeted_user_name, :string 8 | 9 | add_column :deleted_tweets, :retweeted_id, :integer, { :limit => 8 } 10 | add_column :deleted_tweets, :retweeted_content, :string 11 | add_column :deleted_tweets, :retweeted_user_name, :string 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20131107190109_populate_retweet_fields.rb: -------------------------------------------------------------------------------- 1 | class PopulateRetweetFields < ActiveRecord::Migration 2 | def up 3 | Tweet.find_each do |t| 4 | t.save! 5 | end 6 | DeletedTweet.find_each do |t| 7 | t.save! 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20131112174453_add_avatar_columns_to_politician.rb: -------------------------------------------------------------------------------- 1 | class AddAvatarColumnsToPolitician < ActiveRecord::Migration 2 | def self.up 3 | change_table :politicians do |t| 4 | t.has_attached_file :avatar 5 | end 6 | end 7 | 8 | def self.down 9 | drop_attached_file :politicians, :avatar 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20131217182115_add_composite_indexes_for_politican_joins.rb: -------------------------------------------------------------------------------- 1 | class AddCompositeIndexesForPoliticanJoins < ActiveRecord::Migration 2 | def up 3 | add_index :tweets, [:politician_id, :created] 4 | add_index :deleted_tweets, [:politician_id, :created] 5 | end 6 | 7 | def down 8 | remove_index :tweets, [:politician_id, :created] 9 | remove_index :deleted_tweets, [:politician_id, :created] 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20131219173338_add_composite_index_politician_and_created.rb: -------------------------------------------------------------------------------- 1 | class AddCompositeIndexPoliticianAndCreated < ActiveRecord::Migration 2 | def up 3 | add_index :tweets, [:politician_id, :modified] 4 | add_index :deleted_tweets, [:politician_id, :modified] 5 | end 6 | 7 | def down 8 | remove_index :tweets, [:politician_id, :modified] 9 | remove_index :deleted_tweets, [:politician_id, :modified] 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20140303200539_add_gender_to_politician.rb: -------------------------------------------------------------------------------- 1 | class AddGenderToPolitician < ActiveRecord::Migration 2 | def change 3 | change_table :politicians do |t| 4 | t.column :gender, :string, :length => 1, :default => 'U' 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20140612143804_add_identifiers_to_politician.rb: -------------------------------------------------------------------------------- 1 | class AddIdentifiersToPolitician < ActiveRecord::Migration 2 | def change 3 | add_column :politicians, :bioguide_id, :string, :limit => 7 4 | add_column :politicians, :opencivicdata_id, :text 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20160125211055_change_approval_defaults.rb: -------------------------------------------------------------------------------- 1 | class ChangeApprovalDefaults < ActiveRecord::Migration 2 | def self.up 3 | change_column_default :tweets, :approved, true 4 | change_column_default :deleted_tweets, :approved, true 5 | change_column_default :tweets, :reviewed, true 6 | change_column_default :deleted_tweets, :reviewed, true 7 | end 8 | 9 | def self.down 10 | change_column_default :tweets, :approved, false 11 | change_column_default :deleted_tweets, :approved, false 12 | change_column_default :tweets, :reviewed, false 13 | change_column_default :deleted_tweets, :reviewed, false 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) 7 | # Mayor.create(:name => 'Daley', :city => cities.first) -------------------------------------------------------------------------------- /lib/core_ext.rb: -------------------------------------------------------------------------------- 1 | Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].each do |path| 2 | require path 3 | end 4 | 5 | -------------------------------------------------------------------------------- /lib/core_ext/string.rb: -------------------------------------------------------------------------------- 1 | class String 2 | def uncapitalize 3 | case self.length 4 | when 0 then self 5 | when 1 then self.downcase 6 | else [self.first.downcase, self.slice(1, self.length)].join 7 | end 8 | end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /lib/javascript_exports.rb: -------------------------------------------------------------------------------- 1 | module JavascriptExports 2 | @@EXPORTS = HashWithIndifferentAccess.new 3 | 4 | def self.export (k, v) 5 | @@EXPORTS[k] = v 6 | end 7 | 8 | def self.to_s 9 | "".html_safe 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/lib/tasks/.gitkeep -------------------------------------------------------------------------------- /lib/tasks/cache.rake: -------------------------------------------------------------------------------- 1 | namespace :cache do 2 | desc 'Clear the Rails cache' 3 | task :clear => :environment do 4 | Rails.cache.clear 5 | "OK" 6 | end 7 | end 8 | 9 | -------------------------------------------------------------------------------- /lib/tasks/dbfix.rake: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | desc 'Fixes the primary key schema for MySQL deployments' 3 | task :fix_mysql_schema => :environment do 4 | if "mysql2" != ActiveRecord::Base.connection.adapter_name.downcase 5 | puts "You aren't using the mysql2 adapter. You probably don't need to run this command." 6 | next 7 | end 8 | 9 | pkey = Tweet.columns.select(&:primary).first 10 | if pkey.nil? 11 | puts "The Tweet model is missing a primary key. Someting is very wrong." 12 | next 13 | end 14 | 15 | if 8 == pkey.limit 16 | puts "The primary key is already wide enough. Skipping." 17 | next 18 | end 19 | 20 | class WidenTweetPrimaryKey < ActiveRecord::Migration 21 | def self.up 22 | change_column :tweets, :id, :integer, :limit => 8 23 | change_column :deleted_tweets, :id, :integer, :limit => 8 24 | end 25 | end 26 | 27 | WidenTweetPrimaryKey.migrate(:up) 28 | end 29 | end 30 | 31 | Rake::Task['db:schema:load'].enhance do 32 | ::Rake::Task['db:fix_mysql_schema'].invoke 33 | end 34 | 35 | -------------------------------------------------------------------------------- /lib/tasks/export.rake: -------------------------------------------------------------------------------- 1 | namespace :politicians do 2 | require 'csv' 3 | 4 | desc 'Export a politicians as records in a CSV file.' 5 | task :export => :environment do 6 | 7 | # make separator configurable 8 | target_file = ENV.fetch('CSV') rescue abort("You must specify a file with CSV=some_file.csv") 9 | separator = ENV.fetch('CSV_SEP', ',') 10 | CSV.open(target_file, 'wb', :col_sep => separator) do |csv| 11 | csv << [ 12 | 'user_name', 13 | 'account_type', 14 | 'state', 15 | 'party', 16 | 'office', 17 | 'first_name', 18 | 'last_name', 19 | 'middle_name', 20 | 'suffix' 21 | ] 22 | Politician.all.each do |pol| 23 | csv << [ 24 | pol.user_name, 25 | pol.account_type && pol.account_type.name, 26 | pol.state, 27 | pol.party && pol.party.name, 28 | pol.office && pol.office.title, 29 | pol.first_name, 30 | pol.last_name, 31 | pol.middle_name, 32 | pol.suffix 33 | ] 34 | end 35 | end 36 | end 37 | 38 | end 39 | 40 | -------------------------------------------------------------------------------- /lib/tasks/tweets.rake: -------------------------------------------------------------------------------- 1 | namespace :tweets do 2 | desc 'Requeue a tweet from the source JSON' 3 | task :requeue => :environment do 4 | tweet_id = ENV['tweet'] || ENV['TWEET'] 5 | if tweet_id.blank? 6 | return puts 'Expected tweet= argument' 7 | end 8 | 9 | queue_name = Settings.fetch(:beanstalk_queues, {})[:tweets] 10 | if queue_name.blank? 11 | return puts 'Expected :beantalk_queues[:tweets] setting.' 12 | end 13 | 14 | tweet = Tweet.find(tweet_id) 15 | result = RequeueTweet.call(:tweet => tweet, :queue_name => queue_name) 16 | if result.success? && !!ENV['deleted'] 17 | deleted_tweet = DeletedTweet.find(tweet_id) 18 | result = RequeueTweet.call(:tweet => deleted_tweet, :queue_name => queue_name) 19 | end 20 | puts result.to_s 21 | end 22 | 23 | desc 'Export fixtures for minimal model graph focused on a given tweet.' 24 | task :export_fixtures => :environment do 25 | tweet_id = ENV['tweet'] || ENV['TWEET'] 26 | if tweet_id.blank? 27 | return puts 'Expected tweet= argument' 28 | end 29 | 30 | tweet = Tweet.find(tweet_id) 31 | dirpath = File.join(ENV['TMP'] || '/tmp', tweet.id.to_s) 32 | FileUtils.mkdir_p(dirpath) 33 | result = Export::ExportTweetSubgraphFixtures.call(:tweet => tweet, :export_dir => dirpath) 34 | puts result.to_s 35 | end 36 | end 37 | 38 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 | 60 |
    61 |

    The page you were looking for doesn't exist.

    62 |

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

    63 |
    64 | 65 | 66 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | We're sorry, but something went wrong (500) 6 | 7 | 56 | 57 | 58 | 59 | 60 |
    61 |
    62 |

    We're sorry, but something went wrong. Try again?

    63 |
    64 |

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

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /script/reverse: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # == Synopsis 4 | # The reverse_scaffold script executes a Rails scaffold based on an existing table. 5 | # 6 | # 7 | # == Usage 8 | # ruby script/reverse_scaffold table_name model_name 9 | # 10 | # 11 | # == Examples 12 | # ruby script/reverse_scaffold users user 13 | # 14 | # 15 | # == Options 16 | # -h, --help Displays help message 17 | # -m. --model Generates model only 18 | # -r, --rspec Generates rspec_scaffold 19 | # -V, --verbose Outputs the command but doesn't execute it 20 | # -v, --version 21 | # 22 | # 23 | # == Author 24 | # Anthony Heukmes 25 | # http://2dconcept.com 26 | # http://github.com/ahe 27 | # 28 | # 29 | # == Copyright 30 | # Copyright (c) 2009 Anthony Heukmes. Licensed under the MIT License 31 | 32 | require 'rdoc/usage' 33 | require 'optparse' 34 | require File.dirname(__FILE__) + '/../config/environment' 35 | 36 | def display_help 37 | RDoc::usage 38 | exit 39 | end 40 | 41 | def display_version 42 | puts "Rails ActiveRecord Reverse Scaffold version #{SCRIPT_VERSION}" 43 | exit 44 | end 45 | 46 | def configure_table_name(class_name, table_name) 47 | model = <<-CODE 48 | class #{class_name} < ActiveRecord::Base 49 | set_table_name '#{table_name}' 50 | end 51 | CODE 52 | 53 | File.open(File.dirname(__FILE__) + "/../app/models/#{class_name.underscore}.rb", 'w+') {|f| f.write(model) } 54 | end 55 | 56 | SCRIPT_VERSION = 1.0 57 | command_name = 'scaffold' 58 | options = {} 59 | 60 | OptionParser.new do |opts| 61 | opts.on('-h', '--help') { display_help } 62 | opts.on('-m', '--model') { command_name = 'model' } 63 | opts.on('-r', '--rspec') { command_name = 'rspec_scaffold' } 64 | opts.on('-V', '--verbose') { options[:verbose] = true } 65 | opts.on('-v', '--version') { display_version } 66 | end.parse! 67 | 68 | display_help unless ARGV[0] && ARGV[1] 69 | 70 | table_name = ARGV[0] 71 | class_name = ARGV[1].camelize 72 | 73 | eval "class ::#{class_name} < ActiveRecord::Base; set_table_name '#{table_name}' end" 74 | klass = eval "::#{class_name}" 75 | 76 | reverse_scaffold = "rails generate #{command_name} #{class_name.underscore} --skip-migration " 77 | klass.columns.each do |col| 78 | reverse_scaffold << col.name + ":" + col.type.to_s + " " 79 | end 80 | 81 | if options[:verbose] 82 | puts reverse_scaffold 83 | else 84 | system reverse_scaffold 85 | configure_table_name(class_name, table_name) if ARGV[0].downcase != ARGV[1].underscore.pluralize 86 | end -------------------------------------------------------------------------------- /unicorn.rb.example: -------------------------------------------------------------------------------- 1 | # See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete 2 | # documentation. 3 | 4 | worker_processes 2 # this should be >= nr_cpus 5 | pid "/path/to/unicorn.pid" 6 | stderr_path "/path/to/unicorn.log" 7 | stdout_path "/path/to/unicorn.log" 8 | 9 | working_directory "/path/to/current" -------------------------------------------------------------------------------- /vendor/cache/actionmailer-4.2.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/actionmailer-4.2.7.gem -------------------------------------------------------------------------------- /vendor/cache/actionpack-4.2.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/actionpack-4.2.7.gem -------------------------------------------------------------------------------- /vendor/cache/actionpack-action_caching-1.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/actionpack-action_caching-1.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/actionview-4.2.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/actionview-4.2.7.gem -------------------------------------------------------------------------------- /vendor/cache/activejob-4.2.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/activejob-4.2.7.gem -------------------------------------------------------------------------------- /vendor/cache/activemodel-4.2.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/activemodel-4.2.7.gem -------------------------------------------------------------------------------- /vendor/cache/activerecord-4.2.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/activerecord-4.2.7.gem -------------------------------------------------------------------------------- /vendor/cache/activesupport-4.2.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/activesupport-4.2.7.gem -------------------------------------------------------------------------------- /vendor/cache/addressable-2.5.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/addressable-2.5.0.gem -------------------------------------------------------------------------------- /vendor/cache/arel-6.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/arel-6.0.3.gem -------------------------------------------------------------------------------- /vendor/cache/awesome_print-1.7.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/awesome_print-1.7.0.gem -------------------------------------------------------------------------------- /vendor/cache/aws-sdk-2.6.22.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/aws-sdk-2.6.22.gem -------------------------------------------------------------------------------- /vendor/cache/aws-sdk-core-2.6.22.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/aws-sdk-core-2.6.22.gem -------------------------------------------------------------------------------- /vendor/cache/aws-sdk-resources-2.6.22.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/aws-sdk-resources-2.6.22.gem -------------------------------------------------------------------------------- /vendor/cache/aws-sigv4-1.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/aws-sigv4-1.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/axiom-types-0.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/axiom-types-0.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/beanstalk-client-1.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/beanstalk-client-1.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/buftok-0.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/buftok-0.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/builder-3.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/builder-3.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/climate_control-0.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/climate_control-0.0.3.gem -------------------------------------------------------------------------------- /vendor/cache/cocaine-0.5.8.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/cocaine-0.5.8.gem -------------------------------------------------------------------------------- /vendor/cache/coderay-1.1.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/coderay-1.1.1.gem -------------------------------------------------------------------------------- /vendor/cache/coercible-1.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/coercible-1.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/comma-3.2.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/comma-3.2.4.gem -------------------------------------------------------------------------------- /vendor/cache/concurrent-ruby-1.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/concurrent-ruby-1.0.2.gem -------------------------------------------------------------------------------- /vendor/cache/dalli-2.7.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/dalli-2.7.6.gem -------------------------------------------------------------------------------- /vendor/cache/descendants_tracker-0.0.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/descendants_tracker-0.0.4.gem -------------------------------------------------------------------------------- /vendor/cache/domain_name-0.5.20161021.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/domain_name-0.5.20161021.gem -------------------------------------------------------------------------------- /vendor/cache/equalizer-0.0.10.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/equalizer-0.0.10.gem -------------------------------------------------------------------------------- /vendor/cache/erubis-2.7.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/erubis-2.7.0.gem -------------------------------------------------------------------------------- /vendor/cache/faraday-0.9.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/faraday-0.9.2.gem -------------------------------------------------------------------------------- /vendor/cache/gender_detector-1.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/gender_detector-1.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/globalid-0.3.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/globalid-0.3.7.gem -------------------------------------------------------------------------------- /vendor/cache/http-1.0.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/http-1.0.4.gem -------------------------------------------------------------------------------- /vendor/cache/http-cookie-1.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/http-cookie-1.0.3.gem -------------------------------------------------------------------------------- /vendor/cache/http-form_data-1.0.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/http-form_data-1.0.1.gem -------------------------------------------------------------------------------- /vendor/cache/http_parser.rb-0.6.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/http_parser.rb-0.6.0.gem -------------------------------------------------------------------------------- /vendor/cache/httparty-0.10.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/httparty-0.10.2.gem -------------------------------------------------------------------------------- /vendor/cache/i18n-0.7.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/i18n-0.7.0.gem -------------------------------------------------------------------------------- /vendor/cache/ice_nine-0.11.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/ice_nine-0.11.2.gem -------------------------------------------------------------------------------- /vendor/cache/jmespath-1.3.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/jmespath-1.3.1.gem -------------------------------------------------------------------------------- /vendor/cache/json-1.8.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/json-1.8.3.gem -------------------------------------------------------------------------------- /vendor/cache/kgio-2.10.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/kgio-2.10.0.gem -------------------------------------------------------------------------------- /vendor/cache/loofah-2.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/loofah-2.0.3.gem -------------------------------------------------------------------------------- /vendor/cache/mail-2.6.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/mail-2.6.4.gem -------------------------------------------------------------------------------- /vendor/cache/memcache-1.3.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/memcache-1.3.0.gem -------------------------------------------------------------------------------- /vendor/cache/memcache-client-1.8.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/memcache-client-1.8.5.gem -------------------------------------------------------------------------------- /vendor/cache/memoizable-0.4.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/memoizable-0.4.2.gem -------------------------------------------------------------------------------- /vendor/cache/method_source-0.8.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/method_source-0.8.2.gem -------------------------------------------------------------------------------- /vendor/cache/mime-types-3.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/mime-types-3.1.gem -------------------------------------------------------------------------------- /vendor/cache/mime-types-data-3.2016.0521.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/mime-types-data-3.2016.0521.gem -------------------------------------------------------------------------------- /vendor/cache/mini_portile2-2.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/mini_portile2-2.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/minitest-5.9.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/minitest-5.9.1.gem -------------------------------------------------------------------------------- /vendor/cache/multi_json-1.12.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/multi_json-1.12.1.gem -------------------------------------------------------------------------------- /vendor/cache/multi_xml-0.5.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/multi_xml-0.5.5.gem -------------------------------------------------------------------------------- /vendor/cache/multipart-post-2.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/multipart-post-2.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/mysql2-0.3.21.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/mysql2-0.3.21.gem -------------------------------------------------------------------------------- /vendor/cache/naught-1.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/naught-1.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/nokogiri-1.6.8.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/nokogiri-1.6.8.1.gem -------------------------------------------------------------------------------- /vendor/cache/oauth-0.5.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/oauth-0.5.1.gem -------------------------------------------------------------------------------- /vendor/cache/paperclip-2.7.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/paperclip-2.7.0.gem -------------------------------------------------------------------------------- /vendor/cache/pry-0.9.12.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/pry-0.9.12.6.gem -------------------------------------------------------------------------------- /vendor/cache/pry_debug-0.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/pry_debug-0.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/public_suffix-2.0.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/public_suffix-2.0.4.gem -------------------------------------------------------------------------------- /vendor/cache/rack-1.6.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/rack-1.6.5.gem -------------------------------------------------------------------------------- /vendor/cache/rack-test-0.6.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/rack-test-0.6.3.gem -------------------------------------------------------------------------------- /vendor/cache/rails-4.2.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/rails-4.2.7.gem -------------------------------------------------------------------------------- /vendor/cache/rails-deprecated_sanitizer-1.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/rails-deprecated_sanitizer-1.0.3.gem -------------------------------------------------------------------------------- /vendor/cache/rails-dom-testing-1.0.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/rails-dom-testing-1.0.7.gem -------------------------------------------------------------------------------- /vendor/cache/rails-html-sanitizer-1.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/rails-html-sanitizer-1.0.3.gem -------------------------------------------------------------------------------- /vendor/cache/rails_autolink-1.1.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/rails_autolink-1.1.6.gem -------------------------------------------------------------------------------- /vendor/cache/railties-4.2.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/railties-4.2.7.gem -------------------------------------------------------------------------------- /vendor/cache/raindrops-0.17.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/raindrops-0.17.0.gem -------------------------------------------------------------------------------- /vendor/cache/rake-11.3.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/rake-11.3.0.gem -------------------------------------------------------------------------------- /vendor/cache/rmagick-2.16.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/rmagick-2.16.0.gem -------------------------------------------------------------------------------- /vendor/cache/sass-3.4.22.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/sass-3.4.22.gem -------------------------------------------------------------------------------- /vendor/cache/sass-rails-5.0.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/sass-rails-5.0.6.gem -------------------------------------------------------------------------------- /vendor/cache/simple_oauth-0.3.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/simple_oauth-0.3.1.gem -------------------------------------------------------------------------------- /vendor/cache/sitemap_generator-4.3.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/sitemap_generator-4.3.1.gem -------------------------------------------------------------------------------- /vendor/cache/slop-3.6.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/slop-3.6.0.gem -------------------------------------------------------------------------------- /vendor/cache/sprockets-3.7.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/sprockets-3.7.0.gem -------------------------------------------------------------------------------- /vendor/cache/sprockets-rails-3.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/sprockets-rails-3.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/thor-0.19.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/thor-0.19.1.gem -------------------------------------------------------------------------------- /vendor/cache/thread_safe-0.3.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/thread_safe-0.3.5.gem -------------------------------------------------------------------------------- /vendor/cache/tilt-2.0.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/tilt-2.0.5.gem -------------------------------------------------------------------------------- /vendor/cache/twitter-5.16.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/twitter-5.16.0.gem -------------------------------------------------------------------------------- /vendor/cache/twitter-text-1.14.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/twitter-text-1.14.1.gem -------------------------------------------------------------------------------- /vendor/cache/tzinfo-1.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/tzinfo-1.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/unf-0.1.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/unf-0.1.4.gem -------------------------------------------------------------------------------- /vendor/cache/unf_ext-0.0.7.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/unf_ext-0.0.7.2.gem -------------------------------------------------------------------------------- /vendor/cache/unicorn-5.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/unicorn-5.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/virtus-1.0.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/virtus-1.0.5.gem -------------------------------------------------------------------------------- /vendor/cache/will_paginate-3.1.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/propublica/politwoops_sunlight/e1ce513ec75aa2ffc16fd4816630bdc3a1dc6f89/vendor/cache/will_paginate-3.1.5.gem --------------------------------------------------------------------------------