├── .env.example ├── .gitignore ├── .projections.json ├── .pryrc ├── .rspec ├── .rubocop.yml ├── .ruby-gemset ├── .ruby-version ├── .tool-versions ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Procfile ├── README.md ├── Rakefile ├── app ├── adapters │ ├── bcycle_adapter.rb │ ├── beeminder_adapter.rb │ ├── googlefit_adapter.rb │ ├── pocket_adapter.rb │ ├── stackoverflow_adapter.rb │ └── trello_adapter.rb ├── assets │ ├── fonts │ │ └── bootstrap │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ ├── images │ │ ├── .keep │ │ └── logos │ │ │ ├── bcycle.png │ │ │ ├── beeminder.png │ │ │ ├── googlefit.png │ │ │ ├── pocket.png │ │ │ ├── stackoverflow.png │ │ │ └── trello.png │ ├── javascripts │ │ └── application.js │ └── stylesheets │ │ ├── application.css.scss │ │ ├── framework_and_overrides.css.scss │ │ └── main.sass ├── controllers │ ├── application_controller.rb │ ├── authenticated_controller.rb │ ├── callback_controller.rb │ ├── concerns │ │ └── .keep │ ├── credentials_controller.rb │ ├── goals_controller.rb │ ├── main_controller.rb │ └── sessions_controller.rb ├── decorators │ ├── credential_decorator.rb │ ├── goal_decorator.rb │ └── provider_decorator.rb ├── helpers │ └── application_helper.rb ├── mailers │ └── .keep ├── metrics │ ├── bcycle │ │ ├── trip_durations.rb │ │ └── trip_lengths.rb │ ├── beeminder │ │ ├── compose_goals.rb │ │ └── count_datapoints.rb │ ├── googlefit │ │ ├── active_hours.rb │ │ ├── bed_time_lag_minutes.rb │ │ ├── hourly_steps.rb │ │ ├── sleep_duration_hours.rb │ │ └── strength_training_minutes.rb │ ├── pocket │ │ ├── article_days_linear.rb │ │ ├── article_word_days.rb │ │ └── article_words.rb │ ├── stackoverflow │ │ └── reputation.rb │ └── trello │ │ ├── idle_days_exponential.rb │ │ ├── idle_days_linear.rb │ │ ├── idle_hours_average.rb │ │ └── idle_hours_rmp.rb ├── models │ ├── .keep │ ├── base_adapter.rb │ ├── concerns │ │ └── .keep │ ├── credential.rb │ ├── datapoint.rb │ ├── goal.rb │ ├── metric.rb │ ├── metric_repo.rb │ ├── provider.rb │ ├── repo.rb │ ├── score.rb │ └── user.rb ├── services │ ├── datapoints_sync.rb │ └── identity_resolver.rb ├── views │ ├── credentials │ │ └── edit.html.haml │ ├── goals │ │ ├── _beeminder_compose_goals.html.haml │ │ ├── _beeminder_count_datapoints.html.haml │ │ └── edit.html.haml │ ├── layouts │ │ ├── _messages.html.haml │ │ ├── _navigation.html.haml │ │ ├── _navigation_links.html.haml │ │ └── application.html.haml │ └── main │ │ ├── _metrics.html.haml │ │ ├── _my_goals.html.haml │ │ ├── _welcome.html.haml │ │ └── index.html.haml └── workers │ ├── backup_worker.rb │ └── beeminder_worker.rb ├── bin ├── bundle ├── rails ├── rake ├── rspec ├── setup ├── spring ├── update └── yarn ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── content_security_policy.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── new_framework_defaults_5_2.rb │ ├── omniauth.rb │ ├── per_form_csrf_tokens.rb │ ├── providers.rb │ ├── request_forgery_protection.rb │ ├── rollbar.rb │ ├── session_store.rb │ ├── sidekiq.rb │ ├── simple_form.rb │ ├── simple_form_bootstrap.rb │ ├── utc_constant.rb │ └── wrap_parameters.rb ├── locales │ ├── en.yml │ └── simple_form.en.yml ├── newrelic.yml ├── puma.rb ├── routes.rb ├── schedule.rb ├── secrets.yml ├── spring.rb └── storage.yml ├── db ├── migrate │ ├── 20140714204840_create_users.rb │ ├── 20140729165051_create_providers.rb │ ├── 20150719185636_create_goals.rb │ ├── 20150721223451_add_params_to_goals.rb │ ├── 20150907205208_rename_providers_to_credentials.rb │ ├── 20150908234553_add_metric_key_to_goal.rb │ ├── 20150909225051_create_scores.rb │ ├── 20150926194732_change_score_datpoint_id_to_unique.rb │ ├── 20150926210204_add_status_flag_to_goal.rb │ ├── 20160316220158_remove_beeminder_token_from_users_table.rb │ ├── 20160519212602_add_password_to_credential.rb │ └── 20170125204752_add_timestamps_to_goals.rb └── structure.sql ├── lib ├── assets │ └── .keep ├── tasks │ └── .keep └── templates │ └── haml │ └── scaffold │ └── _form.html.haml ├── public ├── 404.html ├── 422.html ├── 500.html ├── favicon.ico ├── humans.txt └── robots.txt ├── spec ├── adapters │ ├── googlefit_adapter_spec.rb │ └── pocket_adapter_spec.rb ├── controllers │ └── sessions_controller_spec.rb ├── factories │ ├── credential_factory.rb │ ├── goal_factory.rb │ └── user_factory.rb ├── features │ ├── googlefit_goals_spec.rb │ ├── home_spec.rb │ ├── providers │ │ └── main_spec.rb │ ├── trello_goals_spec.rb │ └── users │ │ └── session_spec.rb ├── metrics │ ├── beeminder │ │ └── compose_goals_spec.rb │ ├── googlefit │ │ ├── bed_time_lag_minutes_spec.rb │ │ └── hourly_steps_spec.rb │ ├── pocket │ │ └── article_days_linear_spec.rb │ └── trello │ │ └── idle_days_linear_spec.rb ├── models │ ├── datapoint_spec.rb │ └── user_spec.rb ├── rails_helper.rb ├── routing │ └── goals_spec.rb ├── services │ └── identity_resolver_spec.rb ├── spec_helper.rb └── support │ ├── capybara.rb │ ├── factory_girl.rb │ ├── omniauth_test_helpers.rb │ ├── third_party_mocks.rb │ └── vcr_helper.rb └── vendor └── assets ├── javascripts └── .keep └── stylesheets └── .keep /.env.example: -------------------------------------------------------------------------------- 1 | # Add account credentials and API keys here. 2 | # This file should be listed in .gitignore to keep your settings secret! 3 | # Each entry sets a local environment variable. 4 | # For example, setting: 5 | # GMAIL_USERNAME=Your_Gmail_Username 6 | # makes 'Your_Gmail_Username' available as ENV["GMAIL_USERNAME"] 7 | 8 | GMAIL_USERNAME=Your_Username 9 | GMAIL_PASSWORD=Your_Password 10 | DOMAIN_NAME=quantifier.heroku.com 11 | BEEMINDER_PROVIDER_KEY= 12 | BEEMINDER_PROVIDER_SECRET= 13 | POCKET_PROVIDER_KEY= 14 | SECRET_KEY_BASE= 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------- 2 | # Ignore these files when commiting to a git repository. 3 | # 4 | # See http://help.github.com/ignore-files/ for more about ignoring files. 5 | # 6 | # The original version of this file is found here: 7 | # https://github.com/RailsApps/rails-composer/blob/master/files/gitignore.txt 8 | # 9 | # Corrections? Improvements? Create a GitHub issue: 10 | # http://github.com/RailsApps/rails-composer/issues 11 | #---------------------------------------------------------------------------- 12 | .env 13 | 14 | # bundler state 15 | /.bundle 16 | /vendor/bundle/ 17 | /vendor/ruby/ 18 | 19 | # minimal Rails specific artifacts 20 | db/*.sqlite3 21 | /db/*.sqlite3-journal 22 | /log/* 23 | /tmp/* 24 | 25 | # various artifacts 26 | **.war 27 | *.rbc 28 | *.sassc 29 | .redcar/ 30 | .sass-cache 31 | /config/config.yml 32 | /coverage.data 33 | /coverage/ 34 | /db/*.javadb/ 35 | /db/*.sqlite3 36 | /doc/api/ 37 | /doc/app/ 38 | /doc/features.html 39 | /doc/specs.html 40 | /public/cache 41 | /public/assets 42 | /public/stylesheets/compiled 43 | /public/system/* 44 | /spec/tmp/* 45 | /cache 46 | /capybara* 47 | /capybara-*.html 48 | /gems 49 | /specifications 50 | rerun.txt 51 | pickle-email-*.html 52 | .zeus.sock 53 | 54 | # If you find yourself ignoring temporary files generated by your text editor 55 | # or operating system, you probably want to add a global ignore instead: 56 | # git config --global core.excludesfile ~/.gitignore_global 57 | # 58 | # Here are some files you may want to ignore globally: 59 | 60 | # scm revert files 61 | **.orig 62 | 63 | # Mac finder artifacts 64 | .DS_Store 65 | 66 | # Netbeans project directory 67 | /nbproject/ 68 | 69 | # RubyMine project files 70 | .idea 71 | 72 | # Textmate project files 73 | /*.tmproj 74 | 75 | # vim artifacts 76 | **.swp 77 | 78 | # Environment files that may contain sensitive data 79 | .env 80 | .powenv 81 | tags 82 | -------------------------------------------------------------------------------- /.projections.json: -------------------------------------------------------------------------------- 1 | { 2 | "app/services/*.rb": { 3 | "type": "service" 4 | }, 5 | "app/workers/*_worker.rb": { 6 | "type": "worker" 7 | }, 8 | "app/adapters/*.rb": { 9 | "type": "adapter" 10 | }, 11 | "app/metrics/*.rb": { 12 | "type": "metric" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.pryrc: -------------------------------------------------------------------------------- 1 | def usage_report 2 | p %w(all_users with_credentials with_goals) 3 | pp [ User.count, User.joins(:credentials).uniq.count, User.joins(:goals).uniq.count] 4 | 5 | p "Goals per user" 6 | pp User.joins(:goals).includes(:goals).map{ |u| [u.beeminder_user_id, u.goals.count] } 7 | 8 | p "User per provider" 9 | pp Goal.includes(:credential).map{|g| [g.provider.name, g.credential.beeminder_user_id] }.group_by(&:first) 10 | 11 | p "Active Goals" 12 | pp Goal.where(active: true).count 13 | 14 | end 15 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | --require rails_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.3 3 | Exclude: 4 | - 'db/**/*' 5 | - 'config/**/*' 6 | - 'vendor/**/*' 7 | - 'bin/**/*' 8 | Style/StringLiterals: 9 | EnforcedStyle: double_quotes 10 | SupportedStyles: 11 | - single_quotes 12 | - double_quotes 13 | 14 | Style/StringLiteralsInInterpolation: 15 | EnforcedStyle: double_quotes 16 | SupportedStyles: 17 | - single_quotes 18 | - double_quotes 19 | Metrics/LineLength: 20 | Max: 90 21 | Documentation: 22 | Enabled: false 23 | Style/DoubleNegation: 24 | Enabled: false 25 | Style/MultilineBlockChain: 26 | Enabled: false 27 | Style/FrozenStringLiteralComment: 28 | Enabled: false 29 | TrailingCommaInLiteral: 30 | EnforcedStyleForMultiline: comma 31 | Style/IfUnlessModifier: 32 | Enabled: false 33 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | quantifier 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.5.1 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 2.7.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | before_install: 3 | - gem install bundler 4 | rvm: 5 | - "2.7.0" 6 | services: 7 | - postgresql 8 | cache: bundler 9 | env: 10 | - RAILS_ENV=test 11 | script: 12 | - bundle exec rake db:create 13 | - bundle exec rake db:reset 14 | - bundle exec rake spec 15 | addons: 16 | postgresql: "9.4" 17 | code_climate: 18 | repo_token: a3aa46f3d16d10b2fd7566e83a8c9010bd13e96ddf307aec7d69df5cae5756db 19 | 20 | #The following two lines are required to compile mini_racer 21 | sudo: required 22 | dist: trusty 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "activerecord-import" 4 | gem "beeminder" 5 | gem "bootstrap-sass" 6 | 7 | gem "haml-rails" 8 | gem "jquery-rails" 9 | gem "omniauth" 10 | gem "omniauth-oauth2" 11 | gem "pg" 12 | gem "pg_drive", git: "https://github.com/tsubery/pg_drive" 13 | gem "pry-rails" 14 | gem "rails", "~> 5.2" 15 | gem "rollbar" 16 | gem "sass-rails" 17 | gem "sidekiq" 18 | gem "simple_form" 19 | gem "mini_racer" 20 | gem "uglifier", ">= 1.3.0" 21 | gem "whenever", require: false 22 | 23 | # #providers 24 | gem "google-api-client", "0.9" 25 | 26 | # The following line avoids deprecation warning when google-api-client is used 27 | gem "representable", "2.3.0" 28 | 29 | gem "pocket-ruby" 30 | gem "ruby-trello" 31 | gem "my_bcycle" 32 | 33 | gem "oj" #make sure multi-json have a supported backend 34 | 35 | # auth 36 | gem "omniauth-beeminder", branch: "master", git: "https://github.com/beeminder/omniauth-beeminder" 37 | gem "omniauth-google-oauth2" 38 | gem "omniauth-pocket" 39 | gem "omniauth-trello" 40 | 41 | 42 | group :development do 43 | gem "better_errors" 44 | gem "brakeman", require: false 45 | gem "binding_of_caller", platforms: [:mri_21] 46 | gem "html2haml" 47 | gem "hub", require: false 48 | gem "rails_layout" 49 | gem "rb-fchange", require: false 50 | gem "rb-fsevent", require: false 51 | gem "rb-inotify", require: false 52 | gem "rerun", require: false 53 | gem "rubocop", require: false 54 | gem "spring" 55 | end 56 | 57 | group :development, :test do 58 | gem "factory_girl_rails" 59 | gem "awesome_print" 60 | gem "pry-byebug" 61 | gem "pry-rescue" 62 | gem "rspec-rails" 63 | gem "thin" 64 | gem "spring-commands-rspec" 65 | end 66 | 67 | group :test do 68 | gem "capybara" 69 | gem "codeclimate-test-reporter", require: false 70 | gem "poltergeist" 71 | gem "rspec-instafail", require: false 72 | gem "timecop" 73 | gem "vcr" 74 | gem "webmock" 75 | end 76 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/beeminder/omniauth-beeminder 3 | revision: e77759ca5d8210895fbe95b01199e58c74a1bda1 4 | branch: master 5 | specs: 6 | omniauth-beeminder (0.1.2) 7 | omniauth-oauth2 (>= 1.1.1) 8 | 9 | GIT 10 | remote: https://github.com/tsubery/pg_drive 11 | revision: cea0e12b12cdad92a782e9ba101482ea96759363 12 | specs: 13 | pg_drive (0.2.0) 14 | google-api-client (= 0.9) 15 | pg 16 | rails (> 4.0.0, < 6.0.0) 17 | 18 | GEM 19 | remote: https://rubygems.org/ 20 | specs: 21 | actioncable (5.2.4.5) 22 | actionpack (= 5.2.4.5) 23 | nio4r (~> 2.0) 24 | websocket-driver (>= 0.6.1) 25 | actionmailer (5.2.4.5) 26 | actionpack (= 5.2.4.5) 27 | actionview (= 5.2.4.5) 28 | activejob (= 5.2.4.5) 29 | mail (~> 2.5, >= 2.5.4) 30 | rails-dom-testing (~> 2.0) 31 | actionpack (5.2.4.5) 32 | actionview (= 5.2.4.5) 33 | activesupport (= 5.2.4.5) 34 | rack (~> 2.0, >= 2.0.8) 35 | rack-test (>= 0.6.3) 36 | rails-dom-testing (~> 2.0) 37 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 38 | actionview (5.2.4.5) 39 | activesupport (= 5.2.4.5) 40 | builder (~> 3.1) 41 | erubi (~> 1.4) 42 | rails-dom-testing (~> 2.0) 43 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 44 | activejob (5.2.4.5) 45 | activesupport (= 5.2.4.5) 46 | globalid (>= 0.3.6) 47 | activemodel (5.2.4.5) 48 | activesupport (= 5.2.4.5) 49 | activerecord (5.2.4.5) 50 | activemodel (= 5.2.4.5) 51 | activesupport (= 5.2.4.5) 52 | arel (>= 9.0) 53 | activerecord-import (0.17.1) 54 | activerecord (>= 3.2) 55 | activestorage (5.2.4.5) 56 | actionpack (= 5.2.4.5) 57 | activerecord (= 5.2.4.5) 58 | marcel (~> 0.3.1) 59 | activesupport (5.2.4.5) 60 | concurrent-ruby (~> 1.0, >= 1.0.2) 61 | i18n (>= 0.7, < 2) 62 | minitest (~> 5.1) 63 | tzinfo (~> 1.1) 64 | addressable (2.5.0) 65 | public_suffix (~> 2.0, >= 2.0.2) 66 | arel (9.0.0) 67 | ast (2.3.0) 68 | autoprefixer-rails (9.5.1.1) 69 | execjs 70 | awesome_print (1.7.0) 71 | beeminder (0.2.3) 72 | chronic (~> 0.7) 73 | highline (~> 1.6) 74 | json 75 | trollop (~> 2) 76 | better_errors (2.1.1) 77 | coderay (>= 1.0.0) 78 | erubis (>= 2.6.6) 79 | rack (>= 0.9.0) 80 | binding_of_caller (0.7.2) 81 | debug_inspector (>= 0.0.1) 82 | bootstrap-sass (3.4.1) 83 | autoprefixer-rails (>= 5.2.1) 84 | sassc (>= 2.0.0) 85 | brakeman (3.5.0) 86 | builder (3.2.4) 87 | byebug (9.0.6) 88 | capybara (2.12.1) 89 | addressable 90 | mime-types (>= 1.16) 91 | nokogiri (>= 1.3.3) 92 | rack (>= 1.0.0) 93 | rack-test (>= 0.5.4) 94 | xpath (~> 2.0) 95 | chronic (0.10.2) 96 | cliver (0.3.2) 97 | codeclimate-test-reporter (1.0.6) 98 | simplecov 99 | coderay (1.1.1) 100 | concurrent-ruby (1.1.8) 101 | connection_pool (2.2.1) 102 | crack (0.4.3) 103 | safe_yaml (~> 1.0.0) 104 | crass (1.0.6) 105 | daemons (1.2.4) 106 | debug_inspector (0.0.2) 107 | diff-lcs (1.3) 108 | docile (1.1.5) 109 | domain_name (0.5.20170223) 110 | unf (>= 0.0.5, < 1.0.0) 111 | erubi (1.10.0) 112 | erubis (2.7.0) 113 | ethon (0.10.1) 114 | ffi (>= 1.3.0) 115 | eventmachine (1.2.3) 116 | execjs (2.7.0) 117 | factory_girl (4.8.0) 118 | activesupport (>= 3.0.0) 119 | factory_girl_rails (4.8.0) 120 | factory_girl (~> 4.8.0) 121 | railties (>= 3.0.0) 122 | faraday (0.11.0) 123 | multipart-post (>= 1.2, < 3) 124 | faraday_middleware (0.11.0.1) 125 | faraday (>= 0.7.4, < 1.0) 126 | ffi (1.9.25) 127 | globalid (0.4.2) 128 | activesupport (>= 4.2.0) 129 | google-api-client (0.9) 130 | activesupport (>= 3.2) 131 | addressable (~> 2.3) 132 | googleauth (~> 0.5) 133 | httpclient (~> 2.7) 134 | hurley (~> 0.1) 135 | memoist (~> 0.11) 136 | mime-types (>= 1.6) 137 | multi_json (~> 1.11) 138 | representable (~> 2.3.0) 139 | retriable (~> 2.0) 140 | thor (~> 0.19) 141 | googleauth (0.5.1) 142 | faraday (~> 0.9) 143 | jwt (~> 1.4) 144 | logging (~> 2.0) 145 | memoist (~> 0.12) 146 | multi_json (~> 1.11) 147 | os (~> 0.9) 148 | signet (~> 0.7) 149 | haml (5.2.1) 150 | temple (>= 0.8.0) 151 | tilt 152 | haml-rails (2.0.1) 153 | actionpack (>= 5.1) 154 | activesupport (>= 5.1) 155 | haml (>= 4.0.6, < 6.0) 156 | html2haml (>= 1.0.1) 157 | railties (>= 5.1) 158 | hashdiff (0.3.2) 159 | hashie (4.1.0) 160 | highline (1.7.8) 161 | html2haml (2.2.0) 162 | erubis (~> 2.7.0) 163 | haml (>= 4.0, < 6) 164 | nokogiri (>= 1.6.0) 165 | ruby_parser (~> 3.5) 166 | http-cookie (1.0.3) 167 | domain_name (~> 0.5) 168 | httpclient (2.8.3) 169 | hub (1.12.4) 170 | hurley (0.2) 171 | i18n (1.8.9) 172 | concurrent-ruby (~> 1.0) 173 | interception (0.5) 174 | jquery-rails (4.2.2) 175 | rails-dom-testing (>= 1, < 3) 176 | railties (>= 4.2.0) 177 | thor (>= 0.14, < 2.0) 178 | json (2.5.1) 179 | jwt (1.5.6) 180 | libv8 (5.3.332.38.3) 181 | listen (3.1.5) 182 | rb-fsevent (~> 0.9, >= 0.9.4) 183 | rb-inotify (~> 0.9, >= 0.9.7) 184 | ruby_dep (~> 1.2) 185 | little-plugger (1.1.4) 186 | logging (2.1.0) 187 | little-plugger (~> 1.1) 188 | multi_json (~> 1.10) 189 | loofah (2.9.0) 190 | crass (~> 1.0.2) 191 | nokogiri (>= 1.5.9) 192 | mail (2.7.1) 193 | mini_mime (>= 0.1.1) 194 | marcel (0.3.3) 195 | mimemagic (~> 0.3.2) 196 | memoist (0.15.0) 197 | method_source (0.8.2) 198 | mime-types (3.1) 199 | mime-types-data (~> 3.2015) 200 | mime-types-data (3.2016.0521) 201 | mimemagic (0.3.5) 202 | mini_mime (1.0.2) 203 | mini_portile2 (2.5.0) 204 | mini_racer (0.1.8) 205 | libv8 (~> 5.3) 206 | minitest (5.14.4) 207 | multi_json (1.12.1) 208 | multi_xml (0.6.0) 209 | multipart-post (2.0.0) 210 | my_bcycle (0.1.1) 211 | typhoeus (~> 1.0) 212 | netrc (0.11.0) 213 | nio4r (2.5.5) 214 | nokogiri (1.11.1) 215 | mini_portile2 (~> 2.5.0) 216 | racc (~> 1.4) 217 | oauth (0.5.1) 218 | oauth2 (1.3.1) 219 | faraday (>= 0.8, < 0.12) 220 | jwt (~> 1.0) 221 | multi_json (~> 1.3) 222 | multi_xml (~> 0.5) 223 | rack (>= 1.2, < 3) 224 | oj (2.18.2) 225 | omniauth (1.9.1) 226 | hashie (>= 3.4.6) 227 | rack (>= 1.6.2, < 3) 228 | omniauth-google-oauth2 (0.4.1) 229 | jwt (~> 1.5.2) 230 | multi_json (~> 1.3) 231 | omniauth (>= 1.1.1) 232 | omniauth-oauth2 (>= 1.3.1) 233 | omniauth-oauth (1.1.0) 234 | oauth 235 | omniauth (~> 1.0) 236 | omniauth-oauth2 (1.4.0) 237 | oauth2 (~> 1.0) 238 | omniauth (~> 1.2) 239 | omniauth-pocket (0.1.0) 240 | omniauth (~> 1.0) 241 | omniauth-oauth2 (~> 1.0) 242 | omniauth-trello (0.0.4) 243 | multi_json (~> 1.5) 244 | oauth (~> 0.4) 245 | omniauth (~> 1.0) 246 | omniauth-oauth (~> 1.0) 247 | os (0.9.6) 248 | parser (2.4.0.0) 249 | ast (~> 2.2) 250 | pg (0.19.0) 251 | pocket-ruby (0.0.6) 252 | faraday (>= 0.7) 253 | faraday_middleware (~> 0.9) 254 | hashie (>= 0.4.0) 255 | multi_json (~> 1.0, >= 1.0.3) 256 | poltergeist (1.13.0) 257 | capybara (~> 2.1) 258 | cliver (~> 0.3.1) 259 | websocket-driver (>= 0.2.0) 260 | powerpack (0.1.1) 261 | pry (0.10.4) 262 | coderay (~> 1.1.0) 263 | method_source (~> 0.8.1) 264 | slop (~> 3.4) 265 | pry-byebug (3.4.2) 266 | byebug (~> 9.0) 267 | pry (~> 0.10) 268 | pry-rails (0.3.5) 269 | pry (>= 0.9.10) 270 | pry-rescue (1.4.5) 271 | interception (>= 0.5) 272 | pry 273 | public_suffix (2.0.5) 274 | racc (1.5.2) 275 | rack (2.2.3) 276 | rack-protection (2.0.3) 277 | rack 278 | rack-test (1.1.0) 279 | rack (>= 1.0, < 3) 280 | rails (5.2.4.5) 281 | actioncable (= 5.2.4.5) 282 | actionmailer (= 5.2.4.5) 283 | actionpack (= 5.2.4.5) 284 | actionview (= 5.2.4.5) 285 | activejob (= 5.2.4.5) 286 | activemodel (= 5.2.4.5) 287 | activerecord (= 5.2.4.5) 288 | activestorage (= 5.2.4.5) 289 | activesupport (= 5.2.4.5) 290 | bundler (>= 1.3.0) 291 | railties (= 5.2.4.5) 292 | sprockets-rails (>= 2.0.0) 293 | rails-dom-testing (2.0.3) 294 | activesupport (>= 4.2.0) 295 | nokogiri (>= 1.6) 296 | rails-html-sanitizer (1.3.0) 297 | loofah (~> 2.3) 298 | rails_layout (1.0.34) 299 | railties (5.2.4.5) 300 | actionpack (= 5.2.4.5) 301 | activesupport (= 5.2.4.5) 302 | method_source 303 | rake (>= 0.8.7) 304 | thor (>= 0.19.0, < 2.0) 305 | rainbow (2.2.1) 306 | rake (13.0.3) 307 | rb-fchange (0.0.6) 308 | ffi 309 | rb-fsevent (0.9.8) 310 | rb-inotify (0.9.8) 311 | ffi (>= 0.5.0) 312 | redis (3.3.3) 313 | representable (2.3.0) 314 | uber (~> 0.0.7) 315 | rerun (0.11.0) 316 | listen (~> 3.0) 317 | rest-client (2.0.1) 318 | http-cookie (>= 1.0.2, < 2.0) 319 | mime-types (>= 1.16, < 4.0) 320 | netrc (~> 0.8) 321 | retriable (2.1.0) 322 | rollbar (2.14.0) 323 | multi_json 324 | rspec (3.5.0) 325 | rspec-core (~> 3.5.0) 326 | rspec-expectations (~> 3.5.0) 327 | rspec-mocks (~> 3.5.0) 328 | rspec-core (3.5.4) 329 | rspec-support (~> 3.5.0) 330 | rspec-expectations (3.5.0) 331 | diff-lcs (>= 1.2.0, < 2.0) 332 | rspec-support (~> 3.5.0) 333 | rspec-instafail (1.0.0) 334 | rspec 335 | rspec-mocks (3.5.0) 336 | diff-lcs (>= 1.2.0, < 2.0) 337 | rspec-support (~> 3.5.0) 338 | rspec-rails (3.5.2) 339 | actionpack (>= 3.0) 340 | activesupport (>= 3.0) 341 | railties (>= 3.0) 342 | rspec-core (~> 3.5.0) 343 | rspec-expectations (~> 3.5.0) 344 | rspec-mocks (~> 3.5.0) 345 | rspec-support (~> 3.5.0) 346 | rspec-support (3.5.0) 347 | rubocop (0.47.1) 348 | parser (>= 2.3.3.1, < 3.0) 349 | powerpack (~> 0.1) 350 | rainbow (>= 1.99.1, < 3.0) 351 | ruby-progressbar (~> 1.7) 352 | unicode-display_width (~> 1.0, >= 1.0.1) 353 | ruby-progressbar (1.8.1) 354 | ruby-trello (2.0.0) 355 | activemodel (>= 3.2.0) 356 | addressable (~> 2.3) 357 | json 358 | oauth (>= 0.4.5) 359 | rest-client (>= 1.8.0) 360 | ruby_dep (1.5.0) 361 | ruby_parser (3.15.1) 362 | sexp_processor (~> 4.9) 363 | safe_yaml (1.0.4) 364 | sass (3.7.4) 365 | sass-listen (~> 4.0.0) 366 | sass-listen (4.0.0) 367 | rb-fsevent (~> 0.9, >= 0.9.4) 368 | rb-inotify (~> 0.9, >= 0.9.7) 369 | sass-rails (5.0.6) 370 | railties (>= 4.0.0, < 6) 371 | sass (~> 3.1) 372 | sprockets (>= 2.8, < 4.0) 373 | sprockets-rails (>= 2.0, < 4.0) 374 | tilt (>= 1.1, < 3) 375 | sassc (2.0.1) 376 | ffi (~> 1.9) 377 | rake 378 | sexp_processor (4.15.2) 379 | sidekiq (4.2.9) 380 | concurrent-ruby (~> 1.0) 381 | connection_pool (~> 2.2, >= 2.2.0) 382 | rack-protection (>= 1.5.0) 383 | redis (~> 3.2, >= 3.2.1) 384 | signet (0.7.3) 385 | addressable (~> 2.3) 386 | faraday (~> 0.9) 387 | jwt (~> 1.5) 388 | multi_json (~> 1.10) 389 | simple_form (5.0.1) 390 | actionpack (>= 5.0) 391 | activemodel (>= 5.0) 392 | simplecov (0.13.0) 393 | docile (~> 1.1.0) 394 | json (>= 1.8, < 3) 395 | simplecov-html (~> 0.10.0) 396 | simplecov-html (0.10.0) 397 | slop (3.6.0) 398 | spring (2.0.1) 399 | activesupport (>= 4.2) 400 | spring-commands-rspec (1.0.4) 401 | spring (>= 0.9.1) 402 | sprockets (3.7.2) 403 | concurrent-ruby (~> 1.0) 404 | rack (> 1, < 3) 405 | sprockets-rails (3.2.2) 406 | actionpack (>= 4.0) 407 | activesupport (>= 4.0) 408 | sprockets (>= 3.0.0) 409 | temple (0.8.2) 410 | thin (1.7.0) 411 | daemons (~> 1.0, >= 1.0.9) 412 | eventmachine (~> 1.0, >= 1.0.4) 413 | rack (>= 1, < 3) 414 | thor (0.20.3) 415 | thread_safe (0.3.6) 416 | tilt (2.0.10) 417 | timecop (0.8.1) 418 | trollop (2.1.2) 419 | faraday (~> 0.9) 420 | typhoeus (1.1.2) 421 | ethon (>= 0.9.0) 422 | tzinfo (1.2.9) 423 | thread_safe (~> 0.1) 424 | uber (0.0.15) 425 | uglifier (3.1.3) 426 | execjs (>= 0.3.0, < 3) 427 | unf (0.1.4) 428 | unf_ext 429 | unf_ext (0.0.7.2) 430 | unicode-display_width (1.1.3) 431 | vcr (3.0.3) 432 | webmock (2.3.2) 433 | addressable (>= 2.3.6) 434 | crack (>= 0.3.2) 435 | hashdiff 436 | websocket-driver (0.7.3) 437 | websocket-extensions (>= 0.1.0) 438 | websocket-extensions (0.1.5) 439 | whenever (0.9.7) 440 | chronic (>= 0.6.3) 441 | xpath (2.0.0) 442 | nokogiri (~> 1.3) 443 | 444 | PLATFORMS 445 | ruby 446 | 447 | DEPENDENCIES 448 | activerecord-import 449 | awesome_print 450 | beeminder 451 | better_errors 452 | binding_of_caller 453 | bootstrap-sass 454 | brakeman 455 | capybara 456 | codeclimate-test-reporter 457 | factory_girl_rails 458 | google-api-client (= 0.9) 459 | haml-rails 460 | html2haml 461 | hub 462 | jquery-rails 463 | mini_racer 464 | my_bcycle 465 | oj 466 | omniauth 467 | omniauth-beeminder! 468 | omniauth-google-oauth2 469 | omniauth-oauth2 470 | omniauth-pocket 471 | omniauth-trello 472 | pg 473 | pg_drive! 474 | pocket-ruby 475 | poltergeist 476 | pry-byebug 477 | pry-rails 478 | pry-rescue 479 | rails (~> 5.2) 480 | rails_layout 481 | rb-fchange 482 | rb-fsevent 483 | rb-inotify 484 | representable (= 2.3.0) 485 | rerun 486 | rollbar 487 | rspec-instafail 488 | rspec-rails 489 | rubocop 490 | ruby-trello 491 | sass-rails 492 | sidekiq 493 | simple_form 494 | spring 495 | spring-commands-rspec 496 | thin 497 | timecop 498 | uglifier (>= 1.3.0) 499 | vcr 500 | webmock 501 | whenever 502 | 503 | BUNDLED WITH 504 | 2.2.11 505 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Gal Tsubery 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec unicorn -p $PORT -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/tsubery/quantifier.svg?branch=master)](https://travis-ci.org/tsubery/quantifier) 2 | [![Code Climate](https://codeclimate.com/github/tsubery/quantifier/badges/gpa.svg)](https://codeclimate.com/github/tsubery/quantifier) 3 | About 4 | ================ 5 | 6 | This is the code behind [beemind.me][1], a web app I made to automatically send data to [Beeminder][2]. 7 | If you are not familiar with Beeminder, it could be described as "goal tracking with teeth". The basic idea is they take your money if you don't meet the goals you have committed to. 8 | 9 | They can automatically gather data from many 3rd party services that track excercise, sleep, productivity, and many more. 10 | This app adds several automatic integrations by allowing users to sign in with their Beeminder account to [beemind.me][1] and configure one of the supported integrations. See [beemind.me][1] for more details. 11 | 12 | [1]: https://www.beemind.me 13 | [2]: https://www.beeminder.com 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path("../config/application", __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/adapters/bcycle_adapter.rb: -------------------------------------------------------------------------------- 1 | class BcycleAdapter < BaseAdapter 2 | class << self 3 | def required_keys 4 | %i(username password) 5 | end 6 | 7 | def auth_type 8 | :password 9 | end 10 | 11 | def website_link 12 | "https://austin.bcycle.com" 13 | end 14 | 15 | def title 16 | "Austin Bcycle" 17 | end 18 | end 19 | 20 | def client 21 | @client ||= self.class.client(uid, password) 22 | end 23 | 24 | def self.valid_credentials?(credentials) 25 | username, password = credentials.values_at(:uid, :password) 26 | username.present? && password.present? 27 | end 28 | 29 | def self.client(username, password) 30 | MyBcycle::User.new( 31 | username: username, 32 | password: password 33 | ) 34 | end 35 | 36 | def statistics_for_last(duration: 3.days) 37 | cutoff = now - duration 38 | 39 | trips = relevant_months.map(&method(:statistics_for)).reduce(:merge) 40 | trips.select { |ts, _| ts > cutoff }.to_h 41 | end 42 | 43 | private 44 | 45 | def relevant_months 46 | # to cover all timezones we use wider range 47 | # we err on making too many requests than missing some 48 | start_ts = now - 4.days 49 | end_ts = now + 1.day 50 | if end_ts.month != start_ts.month 51 | [start_ts, end_ts] 52 | else 53 | [start_ts] 54 | end 55 | end 56 | 57 | def now 58 | @now ||= UTC.now 59 | end 60 | 61 | def statistics_for(time) 62 | client.statistics_for(time) 63 | rescue MyBcycle::InvalidCredentials 64 | raise AuthorizationError 65 | end 66 | 67 | def uid 68 | credentials.fetch :uid 69 | end 70 | 71 | def password 72 | credentials.fetch :password 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /app/adapters/beeminder_adapter.rb: -------------------------------------------------------------------------------- 1 | class BeeminderAdapter < BaseAdapter 2 | RECENT_INTERVAL = 2.days.ago 3 | MAX_SLUG_LENGNTH = 250 4 | 5 | class << self 6 | def required_keys 7 | %i(token) 8 | end 9 | 10 | def auth_type 11 | :oauth 12 | end 13 | 14 | def website_link 15 | "https://www.beeminder.com" 16 | end 17 | 18 | def title 19 | "Beeminder" 20 | end 21 | end 22 | 23 | def client 24 | @client ||= Beeminder::User.new access_token, auth_type: :oauth 25 | end 26 | 27 | def goals 28 | @goals ||= client.goals 29 | end 30 | 31 | def recent_datapoints(slug) 32 | goal = goals.find { |g| g.slug == slug } 33 | return [] unless goal 34 | goal.datapoints.select do |dp| 35 | dp.timestamp > RECENT_INTERVAL 36 | end 37 | rescue 38 | Rails.logger.error("Failed to fetch datapoints for slug #{slug} of goal #{goal.inspect}") 39 | raise 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/adapters/googlefit_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "google/apis/fitness_v1" 3 | 4 | class GooglefitAdapter < BaseAdapter 5 | ESTIMATED_STEPS_DS = 6 | "derived:com.google.step_count.delta:com.google.android.gms:estimated_steps" 7 | ACTIVITY_SEGMENT = "com.google.activity.segment" 8 | SLEEP_SEGMENT_CODE = 72 9 | ALL_SLEEP_SEGMENT_CODES = [SLEEP_SEGMENT_CODE, 109, 110, 111, 112].freeze 10 | INACTIVE_SEGMENT_CODES = [0, 2, 3, 4] + ALL_SLEEP_SEGMENT_CODES 11 | WEIGHT_TRAINING_CODE = 80 12 | ACTIVITY_AGG = Google::Apis::FitnessV1::AggregateBy.new( 13 | data_type_name: ACTIVITY_SEGMENT 14 | ) 15 | 16 | class << self 17 | def required_keys 18 | %i(token) 19 | end 20 | 21 | def auth_type 22 | :oauth 23 | end 24 | 25 | def website_link 26 | "https://fit.google.com" 27 | end 28 | 29 | def title 30 | "Google Fit" 31 | end 32 | end 33 | 34 | def client 35 | Google::Apis::FitnessV1::FitnessService.new 36 | end 37 | 38 | def fetch_datasource(datasource, from = nil) 39 | client.get_user_data_source_dataset( 40 | "me", datasource, 41 | time_range(from), 42 | options: authorization 43 | ).point || [] 44 | rescue Signet::AuthorizationError 45 | raise AuthorizationError 46 | end 47 | 48 | def fetch_steps(from = nil) 49 | fetch_datasource(ESTIMATED_STEPS_DS, from) 50 | end 51 | 52 | def fetch_active(from = nil) 53 | filter_segments(from) do |activity_id| 54 | INACTIVE_SEGMENT_CODES.exclude?(activity_id) 55 | end 56 | end 57 | 58 | def fetch_sleeps(from) 59 | # for sleeps tz matters so we mandate from arg 60 | filter_segments(from) do |activity_id| 61 | ALL_SLEEP_SEGMENT_CODES.include?(activity_id) 62 | end 63 | end 64 | 65 | def fetch_strength(from = nil) 66 | filter_segments(from) do |activity_id| 67 | activity_id == WEIGHT_TRAINING_CODE 68 | end 69 | end 70 | 71 | def filter_segments(from) 72 | fetch_segments(from).select do |point| 73 | yield(point.value.first.int_val) 74 | end 75 | end 76 | 77 | private 78 | 79 | def fetch_segments(from) 80 | client.aggregate_dataset( 81 | "me", 82 | agg_request(from), 83 | options: authorization 84 | ).bucket&.map do |bucket| 85 | bucket.dataset.first.point.first 86 | end || [] 87 | end 88 | 89 | def agg_request(from) 90 | Google::Apis::FitnessV1::AggregateRequest.new( 91 | aggregate_by: [ACTIVITY_AGG], 92 | bucket_by_activity_segment: true, 93 | start_time_millis: (from || default_from).to_i * 1000, 94 | end_time_millis: UTC.now.to_i * 1000 95 | ) 96 | end 97 | 98 | def time_range(from = nil) 99 | "#{to_nano(from || default_from)}-#{to_nano(UTC.now)}" 100 | end 101 | 102 | def default_from 103 | (UTC.now - 2.days).beginning_of_day 104 | end 105 | 106 | def to_nano(timestamp) 107 | 1_000_000_000 * timestamp.to_i 108 | end 109 | 110 | def authorization 111 | { 112 | authorization: Signet::OAuth2::Client.new( 113 | client_id: Rails.application.secrets.google_provider_key, 114 | client_secret: Rails.application.secrets.google_provider_secret, 115 | refresh_token: credentials["refresh_token"], 116 | scope: %w(https://www.googleapis.com/auth/fitness.activity.read), 117 | token_credential_uri: "https://www.googleapis.com/oauth2/v3/token" 118 | ), 119 | } 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /app/adapters/pocket_adapter.rb: -------------------------------------------------------------------------------- 1 | class PocketAdapter < BaseAdapter 2 | def initialize(*_) 3 | Pocket.configure do |config| 4 | config.consumer_key = Rails.application.secrets.pocket_provider_key 5 | end 6 | super 7 | end 8 | 9 | class << self 10 | def required_keys 11 | %i(token) 12 | end 13 | 14 | def auth_type 15 | :oauth 16 | end 17 | 18 | def website_link 19 | "https://getpocket.com" 20 | end 21 | 22 | def title 23 | "Pocket" 24 | end 25 | end 26 | 27 | def client 28 | Pocket.client access_token: access_token 29 | end 30 | 31 | def articles 32 | articles = list("article") 33 | return [] if articles.empty? 34 | articles.respond_to?(:values) ? articles.values : [] 35 | end 36 | 37 | def list(content_type) 38 | client.retrieve(contentType: content_type).fetch("list") 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/adapters/stackoverflow_adapter.rb: -------------------------------------------------------------------------------- 1 | class StackoverflowAdapter < BaseAdapter 2 | class << self 3 | def required_keys 4 | %i(uid) 5 | end 6 | 7 | def auth_type 8 | :none 9 | end 10 | 11 | def website_link 12 | "http://www.stackoverflow.com" 13 | end 14 | 15 | def title 16 | "StackOverflow" 17 | end 18 | 19 | def valid_credentials?(credentials) 20 | uid = credentials["uid"] 21 | uid.to_i > 0 && uid.to_i.to_s == uid 22 | end 23 | end 24 | 25 | def reputation 26 | uid = credentials.fetch(:uid) 27 | resp = Faraday.get("https://api.stackexchange.com/2.2/users/#{uid.to_i}?site=stackoverflow") 28 | 29 | user_info = JSON.parse(resp.body)["items"]&.first 30 | if user_info 31 | user_info.fetch("reputation") 32 | else 33 | raise "Can't fetch reputation" 34 | end 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /app/adapters/trello_adapter.rb: -------------------------------------------------------------------------------- 1 | require "trello" 2 | class TrelloAdapter < BaseAdapter 3 | class << self 4 | def required_keys 5 | %i(token secret) 6 | end 7 | 8 | def auth_type 9 | :oauth 10 | end 11 | 12 | def website_link 13 | "https://trello.com" 14 | end 15 | 16 | def title 17 | "Trello" 18 | end 19 | end 20 | 21 | def client 22 | Trello::Client.new( 23 | consumer_key: Rails.application.secrets.trello_provider_key, 24 | consumer_secret: Rails.application.secrets.trello_provider_key, 25 | oauth_token: access_token, 26 | oauth_token_secret: access_secret 27 | ) 28 | end 29 | 30 | def cards(list_ids) 31 | list_ids.flat_map do |list_id| 32 | client.find(:list, list_id).cards.map(&:itself) 33 | end 34 | end 35 | 36 | def list_options 37 | client.find(:member, uid).boards(filter: :open).flat_map do |b| 38 | b.lists.map do |list| 39 | joined_name = [b.name, list.name].join("/") 40 | [joined_name, list.id] 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/assets/fonts/bootstrap/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/app/assets/fonts/bootstrap/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /app/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/app/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /app/assets/fonts/bootstrap/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/app/assets/fonts/bootstrap/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /app/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/app/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/logos/bcycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/app/assets/images/logos/bcycle.png -------------------------------------------------------------------------------- /app/assets/images/logos/beeminder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/app/assets/images/logos/beeminder.png -------------------------------------------------------------------------------- /app/assets/images/logos/googlefit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/app/assets/images/logos/googlefit.png -------------------------------------------------------------------------------- /app/assets/images/logos/pocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/app/assets/images/logos/pocket.png -------------------------------------------------------------------------------- /app/assets/images/logos/stackoverflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/app/assets/images/logos/stackoverflow.png -------------------------------------------------------------------------------- /app/assets/images/logos/trello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/app/assets/images/logos/trello.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 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require bootstrap 16 | //= require_tree . 17 | -------------------------------------------------------------------------------- /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 bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/framework_and_overrides.css.scss: -------------------------------------------------------------------------------- 1 | // import the CSS framework 2 | @import "bootstrap-sprockets"; 3 | @import "bootstrap"; 4 | 5 | // make all images responsive by default 6 | img { 7 | @extend .img-responsive; 8 | margin: 0 auto; 9 | width: 150px; 10 | } 11 | // override for the 'Home' navigation link 12 | .navbar-brand { 13 | font-size: inherit; 14 | } 15 | 16 | // THESE ARE EXAMPLES YOU CAN MODIFY 17 | // create your own classes 18 | // to make views framework-neutral 19 | main { 20 | @extend .container; 21 | background-color: #eee; 22 | padding-bottom: 80px; 23 | width: 100%; 24 | margin-top: 51px; // accommodate the navbar 25 | } 26 | section { 27 | @extend .row; 28 | margin-top: 20px; 29 | } 30 | 31 | .provider { 32 | display: inline-block; 33 | text-align: center; 34 | margin-left: 20px; 35 | margin-right: 20px; 36 | white-space: nowrap; 37 | height: 350px; 38 | vertical-align: top; 39 | } 40 | 41 | .h4 { 42 | font-size: 16px; 43 | } 44 | -------------------------------------------------------------------------------- /app/assets/stylesheets/main.sass: -------------------------------------------------------------------------------- 1 | #goals-reload 2 | margin-left: 20px 3 | #main_metrics 4 | li 5 | padding: 0 0 0 15px 6 | .provider 7 | h3 8 | text-align: center 9 | img.logo 10 | max-width: 80% 11 | min-width: 80% 12 | padding: 0 0 30px 0 13 | img.greyed 14 | -webkit-filter: grayscale(100%) 15 | filter: grayscale(100%) 16 | footer 17 | padding: 50px 18 | bottom: 0 19 | width: 100% 20 | text-align: center 21 | .narrow-input 22 | width: 10% 23 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | 4 | helper_method :current_user 5 | helper_method :user_signed_in? 6 | 7 | private 8 | 9 | def current_user 10 | @current_user ||= User.find_by beeminder_user_id: session[:beeminder_user_id] 11 | end 12 | 13 | def user_signed_in? 14 | !!current_user 15 | end 16 | 17 | def authenticate_user! 18 | return if current_user 19 | redirect_to root_url, alert: "You need to sign in for access to this page." 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/controllers/authenticated_controller.rb: -------------------------------------------------------------------------------- 1 | class AuthenticatedController < ApplicationController 2 | before_action :authenticate_user! 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/callback_controller.rb: -------------------------------------------------------------------------------- 1 | class CallbackController < ActionController::Base 2 | 3 | def reload_slug 4 | if params["slug"].nil? 5 | render status: 422, json: {"errors" => ["missing slug parameter"]} 6 | elsif params["username"].nil? 7 | render status: 422, json: {"errors" => ["missing username parameter"]} 8 | elsif (goal = find_by_slug(params)).nil? 9 | render status: 404, json: {"errors" => ["goal not found"]} 10 | else 11 | begin 12 | BeeminderWorker.new.perform(goal_id: goal.id) 13 | render status: 200, json: {} 14 | rescue Timeout::Error 15 | logger.error "Timeout syncing goal ##{goal.id}" 16 | render status: 504, json: {} 17 | end 18 | end 19 | end 20 | 21 | def find_by_slug(params) 22 | Goal.joins(:credential).find_by(slug: params["slug"], credentials: {beeminder_user_id: params["username"]}) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/credentials_controller.rb: -------------------------------------------------------------------------------- 1 | class CredentialsController < AuthenticatedController 2 | helper_method def credential 3 | @_credential ||= 4 | begin 5 | collection = current_user.credentials 6 | credential = collection.find_by(id: params[:id]) 7 | credential ||= collection.find_or_initialize_by( 8 | provider_name: params_provider_name 9 | ) 10 | CredentialDecorator.new(credential) 11 | end 12 | end 13 | 14 | helper_method delegate :provider, to: :credential 15 | 16 | before_action :require_provider 17 | 18 | def new 19 | if credential.persisted? 20 | redirect_to root_path 21 | elsif provider.oauth? 22 | redirect_to "/auth/#{provider.name}" 23 | else 24 | render :edit 25 | end 26 | end 27 | 28 | def create 29 | update 30 | end 31 | 32 | def update 33 | unless credential.update_attributes credential_params 34 | flash[:error] = credential.errors.full_messages.join(" ") 35 | end 36 | redirect_to root_path 37 | end 38 | 39 | def destroy 40 | credential.destroy! 41 | redirect_to root_path, notice: "Credential deleted" 42 | end 43 | 44 | private 45 | 46 | def credential_params 47 | if provider.public? || provider.password_auth? 48 | params.require(:credential).permit(:uid, :password) 49 | else 50 | {} 51 | end 52 | end 53 | 54 | def params_provider_name 55 | params[:provider_name] || (params[:credential] || {})[:provider_name] 56 | end 57 | 58 | def require_provider 59 | render(status: 404) if provider.nil? 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /app/controllers/goals_controller.rb: -------------------------------------------------------------------------------- 1 | class GoalsController < AuthenticatedController 2 | helper_method def goal 3 | @_goal ||= GoalDecorator.new(init_goal || raise(ActiveRecord::RecordNotFound)) 4 | end 5 | 6 | helper_method def provider 7 | @_provider ||= PROVIDERS[params[:provider_name]] 8 | end 9 | 10 | helper_method def metric 11 | @_metric ||= provider&.find_metric(params[:metric_key]) 12 | end 13 | 14 | helper_method def credential 15 | @_credential ||= current_user.credentials.where(provider_name: provider.name).first 16 | end 17 | 18 | helper_method def available_goal_slugs 19 | @_availabe_goal_slugs ||= ( 20 | current_user.client.goals.map(&:slug) - 21 | current_user.goals.pluck(:slug) + 22 | [goal.slug] 23 | ).compact 24 | end 25 | 26 | def edit 27 | if credential.nil? 28 | redirect_to new_credential_path(provider_name: provider.name) 29 | else 30 | render :edit 31 | end 32 | end 33 | 34 | def upsert 35 | if goal.update_attributes goal_params 36 | redirect_to root_path, notice: "Updated successfully!" 37 | else 38 | flash[:error] = goal.errors.full_messages.join(" ") 39 | render :edit 40 | end 41 | end 42 | 43 | def destroy 44 | goal.destroy! 45 | redirect_to root_path, notice: "Deleted successfully!" 46 | end 47 | 48 | def reload 49 | BeeminderWorker.new.perform(beeminder_user_id: current_user.beeminder_user_id) 50 | redirect_to root_path, notice: "Scores updated." 51 | end 52 | 53 | private 54 | 55 | def init_goal 56 | (g_id = params[:id]) && current_user.goals.where(id: g_id).first || 57 | credential.goals.find_or_initialize_by(metric_key: metric.key) 58 | end 59 | 60 | def goal_params 61 | slug_keys = params.dig("goal", "params", "source_slugs")&.keys 62 | params.require(:goal) 63 | .permit(:id, :slug, :params, :active, 64 | params: [ 65 | :exponent, :timezone, :bed_time_hour, :bed_time_minute, 66 | list_ids: [], 67 | source_slugs: slug_keys 68 | ]) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /app/controllers/main_controller.rb: -------------------------------------------------------------------------------- 1 | class MainController < ApplicationController 2 | helper_method def providers 3 | @_providers ||= PROVIDERS.map do |name, provider| 4 | ProviderDecorator.new(provider, credentials[name]) 5 | end 6 | end 7 | 8 | helper_method def goals 9 | @_goals ||= current_user&.goals&.map(&GoalDecorator.method(:new)) 10 | end 11 | 12 | helper_method def credentials 13 | @_credentials ||= Array(current_user&.credentials).map do |c| 14 | [c.provider_name, c] 15 | end.to_h.with_indifferent_access 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < ApplicationController 2 | def new 3 | redirect_to "/auth/beeminder" 4 | end 5 | 6 | def create 7 | auth = request.env.fetch "omniauth.auth" 8 | credential = IdentityResolver.new(current_user, auth).credential 9 | 10 | if credential 11 | session[:beeminder_user_id] = credential.user.beeminder_user_id 12 | flash = "Connected successfully." 13 | else 14 | flash = "Please sign in first." 15 | end 16 | redirect_to root_url, notice: flash 17 | end 18 | 19 | def destroy 20 | reset_session 21 | redirect_to root_url, notice: "Signed out!" 22 | end 23 | 24 | def failure 25 | redirect_to root_url, alert: "Authentication error: #{params[:message].humanize}" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/decorators/credential_decorator.rb: -------------------------------------------------------------------------------- 1 | class CredentialDecorator < DelegateClass(Credential) 2 | def connected_as 3 | info["nickname"] || info["email"] || uid 4 | end 5 | 6 | def status 7 | if connected_as.present? 8 | "Connected as #{connected_as}" 9 | else 10 | "Click to connect" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/decorators/goal_decorator.rb: -------------------------------------------------------------------------------- 1 | class GoalDecorator < DelegateClass(Goal) 2 | include ActionView::Helpers::UrlHelper 3 | # nclude ActionView::Helpers::AssetTagHelper 4 | 5 | def status 6 | active ? "Enabled" : "Disabled" 7 | end 8 | 9 | def last_score 10 | score = scores.order(:timestamp).last 11 | score.nil? ? "none" : score.value 12 | end 13 | 14 | def beeminder_link(beeminder_user_id) 15 | link_to slug, "https://www.beeminder.com/#{beeminder_user_id}/goals/#{slug}" 16 | end 17 | 18 | def delete_link 19 | link_to "Delete", 20 | routes.goal_path(self), 21 | method: :delete, 22 | "data-confirm": "Are you sure?", 23 | class: %i(btn btn-default) 24 | end 25 | 26 | def safe_fetch_scores 27 | fetch_scores 28 | rescue => e 29 | Rails.logger.error e.inspect 30 | Rails.logger.error e.backtrace 31 | msg = "Could not fetch scores." 32 | if e.is_a? BaseAdapter::AuthorizationError 33 | msg += " Please authorize again" 34 | end 35 | [OpenStruct.new(timestamp: "now", value: msg)] 36 | end 37 | 38 | def metric_link 39 | title = [provider.title, metric.title].join(" - ") 40 | link_to title, 41 | ProviderDecorator.new(provider).metric_path(metric), 42 | title: "Click to configure" 43 | end 44 | 45 | private 46 | 47 | def routes 48 | Rails.application.routes.url_helpers 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /app/decorators/provider_decorator.rb: -------------------------------------------------------------------------------- 1 | class ProviderDecorator < DelegateClass(Provider) 2 | include ActionView::Helpers::UrlHelper 3 | include ActionView::Helpers::AssetTagHelper 4 | 5 | attr_accessor :credential 6 | 7 | delegate :status, to: :credential 8 | 9 | def initialize(object, credential = nil) 10 | super(object) 11 | 12 | credential ||= Credential.new 13 | self.credential = CredentialDecorator.new(credential) 14 | end 15 | 16 | def credential_link 17 | if credential? 18 | routes.edit_credential_path(credential) 19 | else 20 | routes.new_credential_path(provider_name: name) 21 | end 22 | end 23 | 24 | def credential? 25 | credential.persisted? 26 | end 27 | 28 | def metric_links 29 | metrics.map do |metric| 30 | link_to metric.title, 31 | metric_path(metric), 32 | title: metric.description + ". Click to add or configure." 33 | end 34 | end 35 | 36 | def metric_path(metric) 37 | "/goals/#{name}/#{metric.key}" 38 | end 39 | 40 | private 41 | 42 | def routes 43 | Rails.application.routes.url_helpers 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/app/mailers/.keep -------------------------------------------------------------------------------- /app/metrics/bcycle/trip_durations.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:bcycle).register_metric :trip_durations do |metric| 2 | metric.title = "Trip duration" 3 | metric.description = "Duration of trips in minutes" 4 | 5 | metric.block = proc do |adapter| 6 | adapter.statistics_for_last.map do |ts, data| 7 | Datapoint.new( 8 | unique: true, 9 | timestamp: ts, 10 | value: data.fetch(:duration) 11 | ) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/metrics/bcycle/trip_lengths.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:bcycle).register_metric :trip_length do |metric| 2 | metric.title = "Trip length" 3 | metric.description = "Length of trips in miles" 4 | 5 | metric.block = proc do |adapter| 6 | adapter.statistics_for_lastmap do |ts, data| 7 | Datapoint.new( 8 | unique: true, 9 | timestamp: ts, 10 | value: data.fetch(:miles) 11 | ) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/metrics/beeminder/compose_goals.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:beeminder).register_metric :compose_goals do |metric| 2 | metric.description = 3 | "Combine multiple goals into one by providing a factor for each goal." \ 4 | "Each datapoint from a source goal will be multiplied by the factor " \ 5 | "and sent to target goal." 6 | metric.title = "Compose goals" 7 | slug_key = "source_slugs" 8 | 9 | metric.block = proc do |adapter, options| 10 | Array(options[slug_key]).flat_map do |slug, factor| 11 | next [] if factor.blank? 12 | adapter.recent_datapoints(slug).map do |dp| 13 | value = dp.value * Float(factor) 14 | [ 15 | dp.timestamp.utc, 16 | value, 17 | "#{slug}: #{value.round(2)}", 18 | ] 19 | end 20 | end.group_by(&:first).map do |ts, entries| 21 | Datapoint.new( 22 | unique: true, 23 | timestamp: ts, 24 | value: entries.sum(&:second), 25 | comment_prefix: entries.map(&:third).join(",") 26 | ) 27 | end 28 | end 29 | 30 | metric.param_errors = proc do |params| 31 | slugs = params[slug_key] 32 | if slugs.is_a?(Hash) 33 | errors = [] 34 | 35 | valid_factors = slugs.values.reject(&:blank?).all? do |factor| 36 | begin 37 | Float(factor) 38 | rescue ArgumentError 39 | false 40 | end 41 | end 42 | valid_slugs = slugs.keys.all? do |key| 43 | key.is_a?(String) && key.length <= BeeminderAdapter::MAX_SLUG_LENGNTH 44 | end 45 | 46 | errors << "All factors must be numbers" unless valid_factors 47 | 48 | errors << "Slug too long" unless valid_slugs 49 | 50 | errors 51 | else 52 | ["Must provide #{slug_key} hash"] 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /app/metrics/beeminder/count_datapoints.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:beeminder).register_metric :count_datapoints do |metric| 2 | metric.description = 3 | "Sends the hourly count of datapoints from selected goals to the target goal" 4 | metric.title = "Count Datapoints" 5 | slug_key = "source_slugs" 6 | 7 | metric.block = proc do |adapter, options| 8 | Array(options[slug_key]).flat_map do |slug, factor| 9 | next [] if factor.blank? 10 | adapter.recent_datapoints(slug).map do |dp| 11 | value = dp.value * Float(factor) 12 | [ 13 | dp.timestamp.utc, 14 | value, 15 | "#{slug}: #{value.round(2)}", 16 | ] 17 | end 18 | end.group_by(&:first).map do |ts, entries| 19 | Datapoint.new( 20 | unique: true, 21 | timestamp: ts, 22 | value: entries.map(&:second).reject(&:zero?).count, 23 | comment_prefix: entries.map(&:third).join(",") 24 | ) 25 | end 26 | end 27 | 28 | metric.param_errors = proc do |params| 29 | slugs = params[slug_key] 30 | if slugs.is_a?(Hash) 31 | errors = [] 32 | 33 | valid_factors = slugs.values.reject(&:blank?).all? do |factor| 34 | begin 35 | Float(factor) 36 | rescue ArgumentError 37 | false 38 | end 39 | end 40 | valid_slugs = slugs.keys.all? do |key| 41 | key.is_a?(String) && key.length < 20 42 | end 43 | 44 | errors << "All factors must be numbers" unless valid_factors 45 | 46 | errors << "Invalid slug" unless valid_slugs 47 | 48 | errors 49 | else 50 | ["Must provide #{slug_key} hash"] 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /app/metrics/googlefit/active_hours.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:googlefit).register_metric :active_hours do |metric| 2 | metric.title = "Active time" 3 | metric.description = "Hours of physical activity" 4 | 5 | metric.block = proc do |adapter| 6 | points = adapter.fetch_active 7 | 8 | points.each_with_object(Hash.new { 0 }) do |point, scores| 9 | ts_start = point.start_time_nanos.to_i / 1e9 10 | ts_end = point.end_time_nanos.to_i / 1e9 11 | hour = Time.zone.at(ts_start).beginning_of_hour 12 | scores[hour] += (ts_end - ts_start) / 3600.0 13 | end.map do |ts, value| 14 | Datapoint.new(unique: true, timestamp: ts, value: value) 15 | end.sort_by(&:timestamp) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/metrics/googlefit/bed_time_lag_minutes.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:googlefit).register_metric :bed_time_lag_minutes do |metric| 2 | metric.title = "Bed time lag" 3 | metric.description = "Minutes after defined bedtime" 4 | 5 | metric.block = proc do |adapter, params| 6 | tz = ActiveSupport::TimeZone.new(params["timezone"].to_s) 7 | bed_time_hour = params["bed_time_hour"].to_i 8 | bed_time_minute = params["bed_time_minute"].to_i 9 | 10 | if bed_time_hour.nil? || bed_time_minute.nil? || tz.nil? 11 | OpenStruct.new(value: "Please configure timezone and reload page") 12 | else 13 | defined_bed_time_minutes = 60 * bed_time_hour + bed_time_minute 14 | 15 | # We want to catch the window of possible sleeps 16 | todays_bed_time = tz.now.beginning_of_day + defined_bed_time_minutes.minutes 17 | todays_bed_time_window_start = todays_bed_time - 12.hours 18 | retro_window_start = todays_bed_time_window_start - 2.days 19 | points = adapter.fetch_sleeps(retro_window_start) 20 | 21 | points.each_with_object({}) do |point, scores| 22 | ts_epoch = point.start_time_nanos.to_i / 1_000_000_000 23 | half_day_minutes = 60 * 12 24 | 25 | bed_time = tz.at(ts_epoch) 26 | actual_bed_time_minutes = 60 * bed_time.hour + bed_time.min 27 | diff = (actual_bed_time_minutes - defined_bed_time_minutes) 28 | remainder = (diff + half_day_minutes) % (24 * 60) 29 | value = [0, remainder - half_day_minutes].max 30 | 31 | day = (bed_time - 12.hours).beginning_of_day 32 | scores[day] = [value, scores[day]].compact.min 33 | end.map do |ts, value| 34 | Datapoint.new(unique: true, timestamp: ts, value: value) 35 | end 36 | end 37 | end 38 | 39 | metric.configuration = proc do |_client, params| 40 | tz = params["timezone"] 41 | bed_time_hour = params["bed_time_hour"] 42 | bed_time_minute = params["bed_time_minute"] 43 | hour_options = (1..24).to_a 44 | minute_options = (0..59).to_a 45 | 46 | [ 47 | [:hour, select_tag("goal[params][bed_time_hour]", 48 | options_for_select(hour_options, 49 | selected: bed_time_hour), 50 | class: "form-control")], 51 | [:minute, select_tag("goal[params][bed_time_minute]", 52 | options_for_select(minute_options, 53 | selected: bed_time_minute), 54 | class: "form-control")], 55 | [:timezone, select_tag("goal[params][timezone]", 56 | time_zone_options_for_select(selected: tz), 57 | class: "form-control")], 58 | ] 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/metrics/googlefit/hourly_steps.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:googlefit).register_metric :hourly_steps do |metric| 2 | metric.title = "Hourly Steps" 3 | metric.description = "Steps taken each hour" 4 | 5 | metric.block = proc do |adapter| 6 | points = adapter.fetch_steps 7 | 8 | points.each_with_object(Hash.new { 0 }) do |point, scores| 9 | ts_epoch = point.start_time_nanos.to_i / 1_000_000_000 10 | hour = Time.zone.at(ts_epoch).beginning_of_hour 11 | scores[hour] += point.value.first.int_val 12 | end.map do |ts, value| 13 | Datapoint.new(unique: true, timestamp: ts, value: value) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/metrics/googlefit/sleep_duration_hours.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:googlefit).register_metric :sleep_duration_hours do |metric| 2 | metric.title = "Sleep Duration" 3 | metric.description = "Hours of sleep" 4 | 5 | metric.block = proc do |adapter, params| 6 | tz = ActiveSupport::TimeZone.new(params["timezone"].to_s) 7 | 8 | if tz.nil? 9 | OpenStruct.new(value: "Please configure timezone and reload page") 10 | else 11 | # We want to catch the window of possible sleeps 12 | retro_window_start = tz.now.beginning_of_day - 2.days - 12.hours 13 | points = adapter.filter_segments(retro_window_start) do |activity_id| 14 | GooglefitAdapter::SLEEP_SEGMENT_CODE == activity_id 15 | end 16 | points.each_with_object({}) do |point, scores| 17 | start_ts = point.start_time_nanos.to_i / 1_000_000_000 18 | end_ts = point.end_time_nanos.to_i / 1_000_000_000 19 | 20 | day = (tz.at(start_ts) - 6.hours).beginning_of_day 21 | 22 | scores[day] ||=0 23 | scores[day] += (end_ts.to_f - start_ts.to_f) / 3_600 24 | end.map do |ts, value| 25 | Datapoint.new(unique: true, timestamp: ts, value: value) 26 | end 27 | end 28 | end 29 | 30 | metric.configuration = proc do |_client, params| 31 | tz = params["timezone"] 32 | 33 | [ 34 | [:timezone, select_tag("goal[params][timezone]", 35 | time_zone_options_for_select(selected: tz), 36 | class: "form-control")], 37 | ] 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/metrics/googlefit/strength_training_minutes.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:googlefit).register_metric :strength_training do |metric| 2 | metric.title = "Strength training" 3 | metric.description = "Minutes of strength training" 4 | 5 | metric.block = proc do |adapter| 6 | points = adapter.fetch_strength 7 | 8 | points.each_with_object(Hash.new { 0 }) do |point, scores| 9 | ts_start = point.start_time_nanos.to_i / 1e9 10 | ts_end = point.end_time_nanos.to_i / 1e9 11 | hour = Time.zone.at(ts_start).beginning_of_hour 12 | scores[hour] += ((ts_end - ts_start) / 60.0).ceil 13 | end.map do |ts, value| 14 | Datapoint.new(unique: true, timestamp: ts, value: value) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/metrics/pocket/article_days_linear.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:pocket).register_metric :article_days_linear do |metric| 2 | metric.description = "The sum of days since each article has been added" 3 | metric.title = "Article backlog" 4 | 5 | metric.block = proc do |adapter| 6 | now_as_epoch = Time.current.utc.to_i 7 | value = adapter.articles.map do |article| 8 | now_as_epoch - article["time_added"].to_i 9 | end.sum / 1.day.to_i 10 | 11 | Datapoint.new(value: value) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/metrics/pocket/article_word_days.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:pocket).register_metric :article_word_days do |metric| 2 | metric.description = "The sum of days since each word in an article has been added" 3 | metric.title = "Article words backlog" 4 | 5 | metric.block = proc do |adapter| 6 | now_as_epoch = Time.current.utc.to_i 7 | value = adapter.articles.map do |article| 8 | age = (now_as_epoch - article["time_added"].to_i) / 1.day.to_i 9 | age * article["word_count"].to_i 10 | end.sum 11 | 12 | Datapoint.new(value: value) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/metrics/pocket/article_words.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:pocket).register_metric :article_words do |metric| 2 | metric.description = "The sum of words in all articles" 3 | metric.title = "Total word count" 4 | 5 | metric.block = proc do |adapter| 6 | value = adapter.articles.map do |article| 7 | article["word_count"].to_i 8 | end.sum 9 | 10 | Datapoint.new(value: value) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/metrics/stackoverflow/reputation.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:stackoverflow).register_metric :reputation do |metric| 2 | metric.title = "Reputation" 3 | metric.description = "Reputation score" 4 | 5 | metric.block = proc do |adapter| 6 | Datapoint.new value: adapter.reputation 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/metrics/trello/idle_days_exponential.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:trello).register_metric :idle_days_exponential do |metric| 2 | metric.description = "Sum the days each card has been idle raised to the chosen power" 3 | metric.title = "Cards backlog Exp" 4 | 5 | metric.block = proc do |adapter, options| 6 | exponent = (options["exponent"] || 2.0).to_f 7 | now_utc = Time.current.utc 8 | list_ids = Array(options["list_ids"]) 9 | cards = adapter.cards(list_ids) 10 | value = cards.map do |card| 11 | age = (now_utc - card.last_activity_date) / 1.day 12 | age**exponent 13 | end.sum 14 | 15 | Datapoint.new value: value.to_i 16 | end 17 | 18 | metric.configuration = proc do |client, params| 19 | list_ids = Array(params["list_ids"]) 20 | exponent = params["exponent"] || 2.0 21 | exponent_options = (1..20).map { |i| i.to_f / 10.0 } 22 | 23 | [ 24 | [:list_ids, select_tag("goal[params][list_ids]", 25 | options_for_select(client.list_options, 26 | selected: list_ids), 27 | multiple: true, 28 | class: "form-control")], 29 | 30 | [:exponent, select_tag("goal[params][exponent]", 31 | options_for_select(exponent_options, 32 | selected: exponent), 33 | class: "form-control")], 34 | ] 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/metrics/trello/idle_days_linear.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:trello).register_metric :idle_days_linear do |metric| 2 | metric.description = "Sum of days each card has been idle" 3 | metric.title = "Cards backlog" 4 | 5 | metric.block = proc do |adapter, options| 6 | now_utc = Time.current.utc 7 | list_ids = Array(options[:list_ids]) 8 | cards = adapter.cards(list_ids) 9 | value = cards.map do |card| 10 | now_utc - card.last_activity_date 11 | end.sum / 1.day 12 | 13 | Datapoint.new(value: value.to_i) 14 | end 15 | 16 | metric.configuration = proc do |client, params| 17 | list_ids = Array(params["list_ids"]) 18 | [ 19 | [:list_ids, select_tag("goal[params][list_ids]", 20 | options_for_select(client.list_options, 21 | selected: list_ids), 22 | multiple: true, 23 | class: "form-control")], 24 | ] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/metrics/trello/idle_hours_average.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:trello).register_metric :idle_hours_average do |metric| 2 | metric.description = "Average time each card has been idle measured in hours" 3 | metric.title = "Card average age" 4 | 5 | metric.block = proc do |adapter, options| 6 | now_utc = Time.current.utc 7 | list_ids = Array(options[:list_ids]) 8 | cards = adapter.cards(list_ids) 9 | 10 | sum = cards.map do |card| 11 | (now_utc - card.last_activity_date) / 1.hour 12 | end.sum 13 | value = sum.zero? ? sum : sum / cards.count 14 | 15 | Datapoint.new value: value.to_i 16 | end 17 | 18 | metric.configuration = proc do |client, params| 19 | list_ids = Array(params["list_ids"]) 20 | [ 21 | [:list_ids, select_tag("goal[params][list_ids]", 22 | options_for_select(client.list_options, 23 | selected: list_ids), 24 | multiple: true, 25 | class: "form-control")], 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/metrics/trello/idle_hours_rmp.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS.fetch(:trello).register_metric :idle_hours_rmp do |metric| 2 | metric.description = 'The root mean power of the ages of the cards measured in hours. 3 | Power of 2 is the same as calculating the "Root Mean Square" of the ages.' 4 | metric.title = "Cards age RMP" 5 | 6 | metric.block = proc do |adapter, options| 7 | exponent = (options["exponent"] || 2.0).to_f 8 | now_utc = Time.current.utc 9 | list_ids = Array(options["list_ids"]) 10 | cards = adapter.cards(list_ids) 11 | sum = cards.map do |card| 12 | age = (now_utc - card.last_activity_date) / 1.hour 13 | age**exponent 14 | end.sum 15 | value = sum.zero? ? sum : Math.sqrt(sum / cards.count) 16 | 17 | Datapoint.new value: value.to_i 18 | end 19 | 20 | metric.configuration = proc do |client, params| 21 | list_ids = Array(params["list_ids"]) 22 | exponent = params["exponent"] || 2.0 23 | exponent_options = (1..100).map { |i| i.to_f / 10.0 } 24 | 25 | [ 26 | [:list_ids, select_tag("goal[params][list_ids]", 27 | options_for_select(client.list_options, 28 | selected: list_ids), 29 | multiple: true, 30 | class: "form-control")], 31 | 32 | [:exponent, select_tag("goal[params][exponent]", 33 | options_for_select(exponent_options, 34 | selected: exponent), 35 | class: "form-control")], 36 | ] 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/app/models/.keep -------------------------------------------------------------------------------- /app/models/base_adapter.rb: -------------------------------------------------------------------------------- 1 | class BaseAdapter 2 | InvalidCredentials = Class.new(StandardError) 3 | AuthorizationError = Class.new(StandardError) 4 | 5 | def initialize(credentials) 6 | @credentials = credentials.with_indifferent_access 7 | validate_credentials! 8 | end 9 | 10 | def self.valid_credentials?(credentials) 11 | credentials.values_at(*required_keys).all?(&:present?) 12 | end 13 | 14 | private 15 | 16 | attr_reader :credentials 17 | def required_keys 18 | raise NotImplementedError 19 | end 20 | 21 | def validate_credentials! 22 | valid = self.class.valid_credentials?(credentials) 23 | raise(InvalidCredentials, credentials.to_s) unless valid 24 | end 25 | 26 | def access_token 27 | credentials.fetch :token 28 | end 29 | 30 | def access_secret 31 | credentials.fetch :secret 32 | end 33 | 34 | def uid 35 | credentials.fetch :uid 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/credential.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: credentials 4 | # 5 | # id :integer not null, primary key 6 | # beeminder_user_id :string not null 7 | # provider_name :string not null 8 | # uid :string default(""), not null 9 | # info :json not null 10 | # credentials :json not null 11 | # extra :json not null 12 | # created_at :datetime 13 | # updated_at :datetime 14 | # password :string default(""), not null 15 | # 16 | 17 | class Credential < ActiveRecord::Base 18 | validates :uid, :user, presence: true 19 | 20 | belongs_to :user, primary_key: :beeminder_user_id, foreign_key: :beeminder_user_id 21 | has_many :goals, dependent: :destroy 22 | 23 | def client 24 | authorization = credentials.merge( 25 | uid: uid, 26 | password: password 27 | ).with_indifferent_access 28 | provider.adapter.new(authorization) 29 | end 30 | 31 | def provider 32 | PROVIDERS.fetch(provider_name) 33 | end 34 | 35 | def access_token 36 | credentials.fetch "token" 37 | end 38 | 39 | def valid_access_token 40 | errors.add(:credentials, "missing token key") unless credentials["token"] 41 | end 42 | 43 | def access_secret 44 | credentials.fetch "secret" 45 | end 46 | 47 | def valid_access_secret 48 | errors.add(:credentials, "missing token key") unless credentials["token"] 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /app/models/datapoint.rb: -------------------------------------------------------------------------------- 1 | class Datapoint 2 | attr_reader :timestamp, :value, :id, :unique, :comment_prefix 3 | include Comparable 4 | 5 | alias eql? == 6 | 7 | def initialize(id: nil, unique: false, timestamp: nil, 8 | value:, comment_prefix: "") 9 | @id = id.to_s 10 | @timestamp = timestamp 11 | @value = value.to_d 12 | @unique = unique 13 | @comment_prefix = comment_prefix 14 | end 15 | 16 | def to_beeminder 17 | Beeminder::Datapoint 18 | .new value: value, 19 | timestamp: timestamp, 20 | comment: "#{comment_prefix} beemind.me for #{timestamp} @ #{Time.current}" 21 | end 22 | 23 | def <=>(other) 24 | return nil unless other.instance_of?(self.class) 25 | @value.<=>(other.value) if @id == other.id && @timestamp == other.timestamp 26 | end 27 | 28 | def hash 29 | [self.class, @id, @timestamp, @value].hash 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/models/goal.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: goals 4 | # 5 | # id :integer not null, primary key 6 | # credential_id :integer not null 7 | # slug :string not null 8 | # last_value :float 9 | # params :json not null 10 | # metric_key :string not null 11 | # active :boolean default(TRUE), not null 12 | # fail_count :integer default(0), not null 13 | # 14 | 15 | class Goal < ActiveRecord::Base 16 | belongs_to :credential 17 | has_many :scores, dependent: :destroy 18 | has_one :user, through: :credential 19 | 20 | delegate :provider, to: :credential 21 | 22 | validates :slug, presence: :true 23 | validate :valid_params 24 | 25 | scope :active, -> { where(active: true) } 26 | 27 | def metric 28 | provider.find_metric(metric_key) 29 | end 30 | 31 | def sync 32 | transaction { with_lock { sync_process } } 33 | end 34 | 35 | def beeminder_goal 36 | user.beeminder_goal(slug) 37 | rescue User::SlugNotFound 38 | Rails.logger.warn "Count not find slug #{slug} for user #{user.inspect}" 39 | nil 40 | end 41 | 42 | def fetch_scores 43 | Array(metric.call(credential.client, options)) 44 | end 45 | 46 | private 47 | 48 | def sync_process 49 | if (bgoal = beeminder_goal) 50 | calculated = fetch_scores 51 | stored = scores.order(:timestamp).map(&:to_datapoint) 52 | syncher = DatapointsSync.new(calculated, stored, bgoal) 53 | syncher.call 54 | store_scores syncher.storable 55 | else 56 | update! active: false 57 | end 58 | end 59 | 60 | def store_scores(datapoints) 61 | return if datapoints.empty? 62 | scores.delete_all 63 | columns = %i(goal_id unique timestamp value) 64 | score_records = datapoints.map do |datapoint| 65 | [id, 66 | datapoint.unique, 67 | datapoint.timestamp || Time.zone.now, 68 | datapoint.value] 69 | end 70 | 71 | Score.import columns, score_records 72 | end 73 | 74 | def options 75 | params.with_indifferent_access 76 | end 77 | 78 | def valid_params 79 | errors[:params] = metric.param_errors(params) 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /app/models/metric.rb: -------------------------------------------------------------------------------- 1 | class Metric 2 | attr_reader :key, :block 3 | attr_accessor :block, :description, :title, :configuration 4 | attr_writer :param_errors 5 | 6 | def initialize(key) 7 | @key = key 8 | @configuration = proc { [] } 9 | @param_errors = proc { [] } 10 | end 11 | 12 | def call(*args) 13 | block.call(*args) 14 | end 15 | 16 | def param_errors(params) 17 | @param_errors.call(params) 18 | end 19 | 20 | def valid? 21 | [key, block, description, title].all? 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/models/metric_repo.rb: -------------------------------------------------------------------------------- 1 | class MetricRepo 2 | include Repo 3 | 4 | def metrics 5 | collection.values 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/provider.rb: -------------------------------------------------------------------------------- 1 | class Provider 2 | attr_reader :auth_type, :adapter, :key 3 | delegate :find, :metrics, to: :metrics_repo 4 | delegate :auth_type, :title, :website_link, to: :adapter 5 | alias find_metric find 6 | alias name key 7 | 8 | def initialize(key, adapter) 9 | @adapter = adapter 10 | @key = key 11 | @metrics_repo = MetricRepo.new 12 | %i(none oauth password).include?(auth_type) || raise("Unknown auth_type #{auth_type}") 13 | end 14 | 15 | def oauth? 16 | :oauth == auth_type 17 | end 18 | 19 | def public? 20 | :none == auth_type 21 | end 22 | 23 | def password_auth? 24 | :password == auth_type 25 | end 26 | 27 | def register_metric(key) 28 | new_metric = Metric.new(key) 29 | yield new_metric 30 | raise "Invalid metric #{key}" unless new_metric.valid? 31 | metrics_repo.store key, new_metric 32 | end 33 | 34 | def load_metrics 35 | Dir["app/metrics/#{key}/*.rb"].each { |f| load Rails.root.join(f) } 36 | end 37 | 38 | private 39 | 40 | attr_reader :metrics_repo 41 | end 42 | -------------------------------------------------------------------------------- /app/models/repo.rb: -------------------------------------------------------------------------------- 1 | module Repo 2 | def find(key) 3 | collection[key.to_sym] 4 | end 5 | 6 | def find!(key) 7 | known_key = key.respond_to?(:to_sym) && collection.key?(key.to_sym) 8 | raise "Unknown key #{key}" unless known_key 9 | find(key) 10 | end 11 | 12 | def store(key, object) 13 | return nil if key.nil? 14 | collection[key.to_sym] = object 15 | end 16 | 17 | delegate :keys, to: :collection 18 | 19 | private 20 | 21 | def collection 22 | @collection ||= {} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/score.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: scores 4 | # 5 | # id :integer not null, primary key 6 | # value :float not null 7 | # timestamp :datetime not null 8 | # goal_id :integer 9 | # created_at :datetime 10 | # updated_at :datetime 11 | # unique :boolean 12 | # 13 | 14 | class Score < ActiveRecord::Base 15 | belongs_to :goal 16 | 17 | def to_datapoint 18 | Datapoint.new( 19 | timestamp: timestamp, 20 | value: value, 21 | unique: unique 22 | ) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # beeminder_user_id :string not null, primary key 6 | # created_at :datetime 7 | # updated_at :datetime 8 | # 9 | 10 | class User < ActiveRecord::Base 11 | has_many :credentials, foreign_key: :beeminder_user_id 12 | has_many :goals, through: :credentials 13 | 14 | self.primary_key = :beeminder_user_id 15 | 16 | SlugNotFound = Class.new(StandardError) 17 | 18 | def self.find_by_provider_attrs(attrs) 19 | User.joins(:providers).find_by(identities: attrs) 20 | end 21 | 22 | def self.upsert(id, token) 23 | user = find_or_initialize_by(beeminder_user_id: id) 24 | user.beeminder_token = token 25 | user.tap(&:save!) 26 | end 27 | 28 | def client 29 | @client ||= BeeminderAdapter.new(beeminder_credentials.credentials).client 30 | end 31 | 32 | def beeminder_credentials 33 | credentials.find_by(provider_name: :beeminder) 34 | end 35 | 36 | def beeminder_goal(slug) 37 | client.goal(slug) 38 | rescue RuntimeError => e 39 | if e.message =~ /request failed/ && 40 | e.message =~ /404/ 41 | raise SlugNotFound 42 | end 43 | raise 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/services/datapoints_sync.rb: -------------------------------------------------------------------------------- 1 | class DatapointsSync 2 | def initialize(calculated, stored, beeminder_goal) 3 | @stored = stored 4 | @calculated = calculated 5 | @beeminder_goal = beeminder_goal 6 | last_value = stored.last && stored.last.value 7 | @new_datapoints = (calculated - stored).reject do |dp| 8 | dp.timestamp.nil? && last_value == dp.value 9 | end 10 | end 11 | 12 | def call 13 | to_delete = overlapping_datapoints(new_datapoints, stored) 14 | transmit new_datapoints 15 | to_delete.each(&beeminder_goal.method(:delete)) 16 | end 17 | 18 | def storable 19 | return [] if new_datapoints.empty? 20 | calculated 21 | end 22 | 23 | private 24 | 25 | attr_reader :stored, :calculated, :new_datapoints, :beeminder_goal 26 | 27 | def transmit(datapoints) 28 | return if datapoints.empty? 29 | beeminder_goal.add datapoints.map(&:to_beeminder) 30 | end 31 | 32 | def overlapping_datapoints(new_datapoints, stored) 33 | timestamps = overlapping_timestamps(new_datapoints, stored) 34 | return [] if timestamps.empty? 35 | beeminder_goal.datapoints.select do |dp| 36 | timestamps.include?(dp.timestamp) 37 | end 38 | end 39 | 40 | def overlapping_timestamps(new_datapoints, stored) 41 | (new_datapoints.map(&:timestamp) & stored.map(&:timestamp)).to_set 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/services/identity_resolver.rb: -------------------------------------------------------------------------------- 1 | class IdentityResolver 2 | attr_reader :flash, :credential 3 | 4 | def initialize(current_user, auth) 5 | @auth = auth 6 | @uid = auth.fetch("uid") 7 | @provider_name = auth.fetch("provider") 8 | @current_user = current_user 9 | 10 | set_credential 11 | end 12 | 13 | private 14 | 15 | attr_reader :auth, :uid, :provider_name, :current_user 16 | 17 | def set_credential 18 | @credential = find_credential 19 | return unless current_user || session_credential? 20 | @credential ||= create_credential(current_user) 21 | update_credential 22 | end 23 | 24 | def find_credential 25 | Credential.find_by( 26 | provider_name: provider_name, 27 | uid: uid 28 | ) 29 | end 30 | 31 | def session_credential? 32 | "beeminder" == provider_name 33 | end 34 | 35 | def create_credential(user) 36 | user ||= User.find_or_create_by!(beeminder_user_id: uid) 37 | Credential.create!( 38 | beeminder_user_id: user.beeminder_user_id, 39 | provider_name: provider_name, 40 | uid: @uid 41 | ) 42 | end 43 | 44 | def update_credential 45 | @credential.update_attributes! @auth.slice("info", "credentials", "extra").to_h 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/views/credentials/edit.html.haml: -------------------------------------------------------------------------------- 1 | .container 2 | .row 3 | %h3 4 | = credential.status 5 | = link_to "Delete", credential, method: :delete, "data-confirm": "This action will disconnect all goals for this provider. Are you sure?", class: %(btn btn-default) 6 | .row 7 | .col-lg-4 8 | - if provider.public? || provider.password_auth? 9 | %h3 10 | Configure Provider 11 | = form_for credential do |f| 12 | = f.hidden_field :provider_name, value: provider.name 13 | .form-group 14 | = f.label :user_id 15 | = f.text_field :uid, class: "form-control" 16 | - if provider.password_auth? 17 | .form-group 18 | = f.label :password 19 | = f.password_field :password, class: "form-control" 20 | = f.submit "Save", class: %(btn btn-default) 21 | 22 | -------------------------------------------------------------------------------- /app/views/goals/_beeminder_compose_goals.html.haml: -------------------------------------------------------------------------------- 1 | %h3 Source Goals 2 | - credential.client.goals.each do |g| 3 | - slugs = params["source_slugs"] || {} 4 | .from-group 5 | = text_field_tag "goal[params][source_slugs][#{g.slug}]", slugs[g.slug], class: " narrow-input" 6 | = pf.label "X #{g.slug}" 7 | -------------------------------------------------------------------------------- /app/views/goals/_beeminder_count_datapoints.html.haml: -------------------------------------------------------------------------------- 1 | %h3 Source Goals 2 | - credential.client.goals.each do |g| 3 | - slugs = params["source_slugs"] || {} 4 | .from-group 5 | = check_box_tag "goal[params][source_slugs][#{g.slug}]", "1", slugs[g.slug], class: " narrow-input" 6 | = pf.label "#{g.slug}" 7 | -------------------------------------------------------------------------------- /app/views/goals/edit.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :title do 2 | Setup 3 | = [ provider.title, metric.title ].join(" - ") 4 | end 5 | .container 6 | .row 7 | %h1 8 | = provider.title 9 | = metric.title 10 | = goal.delete_link if goal.persisted? 11 | .col-lg-9 12 | %h3 13 | Decription 14 | = metric.description 15 | .row 16 | .col-lg-4 17 | %h3 Goal Configuration 18 | = form_for goal, {method: :post, url: upsert_goals_path(provider.name, metric.key), html: {role: :form}} do |f| 19 | .form-group 20 | = f.label :beeminder_slug 21 | = f.select :slug, available_goal_slugs, { prompt: "Select Beeminder Slug"},{ class: "form-control" } 22 | .form-group 23 | = f.check_box :active 24 | = f.label :active, "Enabled" 25 | = f.fields_for :params do |pf| 26 | - extra_configuration = self.instance_exec(credential.client, f.object.params, pf, &metric.configuration) rescue [] 27 | - extra_configuration.each do |(label, input)| 28 | .form-group 29 | = pf.label label 30 | = input 31 | - template_name = "goals/#{provider.name}_#{metric.key}" 32 | - if lookup_context.template_exists?(template_name, [], true) 33 | = render template_name, pf: pf, params: f.object.params 34 | = f.submit "Save", class: %i(btn btn-default) 35 | .col-lg-1 36 | .col-lg-4 37 | %h3 Current Score 38 | %table.table.table-responsive#current-score 39 | %thead 40 | %tr 41 | %td Timestamp 42 | %td Value 43 | %tbody 44 | - goal.safe_fetch_scores.each do |datapoint| 45 | %tr 46 | %td= datapoint.timestamp || "Now" 47 | %td= datapoint.value 48 | 49 | -------------------------------------------------------------------------------- /app/views/layouts/_messages.html.haml: -------------------------------------------------------------------------------- 1 | -# Rails flash messages styled for Bootstrap 3.0 2 | - flash.each do |name, msg| 3 | - if msg.is_a?(String) 4 | %div{:class => "alert alert-#{name.to_s == 'notice' ? 'success' : 'danger'}"} 5 | %button.close{"aria-hidden" => "true", "data-dismiss" => "alert", :type => "button"} × 6 | = content_tag :div, msg, :id => "flash_#{name}" 7 | -------------------------------------------------------------------------------- /app/views/layouts/_navigation.html.haml: -------------------------------------------------------------------------------- 1 | -# navigation styled for Bootstrap 3.0 2 | %nav.navbar.navbar-default.navbar-fixed-top 3 | .container 4 | .navbar-header 5 | %button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", :type => "button"} 6 | %span.sr-only Toggle navigation 7 | %span.icon-bar 8 | %span.icon-bar 9 | %span.icon-bar 10 | = link_to 'Home', root_path, class: 'navbar-brand' 11 | .collapse.navbar-collapse 12 | %ul.nav.navbar-nav 13 | = render 'layouts/navigation_links' 14 | -------------------------------------------------------------------------------- /app/views/layouts/_navigation_links.html.haml: -------------------------------------------------------------------------------- 1 | %li 2 | - if user_signed_in? 3 | = link_to 'Sign out', signout_path 4 | - else 5 | = link_to 'Sign in', signin_path 6 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %meta{:name => "viewport", :content => "width=device-width, initial-scale=1.0"} 5 | %title= content_for?(:title) ? yield(:title) : 'Beemind.me' 6 | %meta{:name => "description", :content => "#{content_for?(:description) ? yield(:description) : 'Beemind.me'}"} 7 | = stylesheet_link_tag 'application', media: 'all' 8 | = javascript_include_tag 'application' 9 | = csrf_meta_tags 10 | %body 11 | %header 12 | = render 'layouts/navigation' 13 | %main 14 | .container 15 | = render 'layouts/messages' 16 | = yield 17 | 18 | %footer 19 | .container 20 | %p 21 | © 22 | %a{ href: "https://github.com/tsubery" } Gal Tsubery. 23 | This software is 24 | %a{ href: "https://github.com/tsubery/quantifier/" } free 25 | under 26 | %a{ href: "https://github.com/tsubery/quantifier/blob/master/LICENSE"} MIT License 27 | \. 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/views/main/_metrics.html.haml: -------------------------------------------------------------------------------- 1 | %h2 2 | Supported integrations 3 | .row-fluid#integrations 4 | - providers.each do |provider| 5 | .provider 6 | = link_to provider.website_link do 7 | %h3.logo-header= provider.title 8 | = link_to provider.credential_link do 9 | - greyed = current_user && !provider.credential? 10 | = image_tag("logos/#{provider.name}.png", 11 | alt: "#{provider.name} Logo", 12 | class: "logo #{greyed && "greyed"}", 13 | title: provider.status) 14 | - provider.metric_links.each do |link| 15 | %h4 16 | = link 17 | -------------------------------------------------------------------------------- /app/views/main/_my_goals.html.haml: -------------------------------------------------------------------------------- 1 | %h2 2 | Configured Goals 3 | = link_to "Reload", reload_goals_path, method: :post, class: %i(btn btn-default), id: "goals-reload" 4 | %table.table-bordered.table#configured-goals 5 | %thead 6 | %tr 7 | %td Name 8 | %td Status 9 | %td beeminder 10 | %td Last score 11 | %tbody 12 | - goals.each do |goal| 13 | %tr 14 | %td= goal.metric_link 15 | %td= goal.status 16 | %td= goal.beeminder_link(current_user.beeminder_user_id) 17 | %td= goal.last_score 18 | 19 | -------------------------------------------------------------------------------- /app/views/main/_welcome.html.haml: -------------------------------------------------------------------------------- 1 | .jumbotron 2 | %h2 Welcome to Beemind.me 3 | %p 4 | This app complements 5 | %a{href: "http://beeminder.com"} Beeminder 6 | by adding more automatic data sources. 7 | %br Check the list below to see what is currently supported. 8 | %p 9 | Have any questions? 10 | 11 | Ask in Beeminder's 12 | = succeed "." do 13 | = link_to "Forum", "http://forum.beeminder.com" 14 | -------------------------------------------------------------------------------- /app/views/main/index.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :title do 2 | Beemind.me - Home 3 | 4 | .row 5 | = render "welcome" 6 | .row 7 | - if current_user && current_user.goals.count > 0 8 | = render "my_goals" 9 | .container 10 | = render "metrics" 11 | - unless current_user 12 | .row-fluid 13 | %p 14 | To start, 15 | =link_to "sign in", signin_path 16 | with your Beeminder account. 17 | 18 | -------------------------------------------------------------------------------- /app/workers/backup_worker.rb: -------------------------------------------------------------------------------- 1 | class BackupWorker 2 | include Sidekiq::Worker 3 | 4 | def perform 5 | Timeout::timeout(120) { 6 | PgDrive.perform 7 | } 8 | rescue Timeout::Error 9 | logger.error("Backup timeout") 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/workers/beeminder_worker.rb: -------------------------------------------------------------------------------- 1 | require "trello" # it is not required by default for some reason 2 | 3 | class BeeminderWorker 4 | include Sidekiq::Worker 5 | 6 | def perform(beeminder_user_id: nil, goal_id: nil) 7 | if goal_id 8 | filter = { id: goal_id } 9 | elsif beeminder_user_id 10 | filter = { users: { beeminder_user_id: beeminder_user_id } } 11 | else 12 | filter = {} 13 | end 14 | 15 | 16 | Goal.where(active: true) 17 | .joins(:user) 18 | .joins(:credential) 19 | .where(filter) 20 | .order("credentials.provider_name = 'beeminder' ASC") 21 | .find_each(&method(:safe_sync)) 22 | end 23 | 24 | private 25 | 26 | def safe_sync(goal) 27 | Timeout::timeout(90) { 28 | goal.sync 29 | } 30 | rescue => e 31 | Rollbar.error(e, goal_id: goal.id) 32 | logger.error e.backtrace 33 | logger.error e.inspect 34 | goal.update fail_count: (goal.fail_count + 1) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require 'bundler/setup' 8 | load Gem.bin_path('rspec-core', 'rspec') 9 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a starting point to setup your application. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! 'bin/rails db:setup' 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! 'bin/rails log:clear tmp:clear' 33 | 34 | puts "\n== Restarting application server ==" 35 | system! 'bin/rails restart' 36 | end 37 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) 11 | Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) } 12 | gem 'spring', match[1] 13 | require 'spring/binstub' 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a way to update your development environment automatically. 14 | # Add necessary update steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | puts "\n== Updating database ==" 24 | system! 'bin/rails db:migrate' 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! 'bin/rails log:clear tmp:clear' 28 | 29 | puts "\n== Restarting application server ==" 30 | system! 'bin/rails restart' 31 | end 32 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | begin 5 | exec "yarnpkg", *ARGV 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path("../config/environment", __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Quantifier 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 5.0 13 | 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration can go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded after loading 17 | # the framework and any gems in your application. 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: quantifier_production 11 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 8.2 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On Mac OS X with macports: 6 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 7 | # On Windows: 8 | # gem install pg 9 | # Choose the win32 build. 10 | # Install PostgreSQL and put its /bin directory on your path. 11 | # 12 | # Configure Using Gemfile 13 | # gem 'pg' 14 | # 15 | development: &default 16 | adapter: postgresql 17 | encoding: unicode 18 | database: quantifier_development 19 | pool: 25 20 | username: <%= ENV['PGUSER'] %> 21 | password: <%= ENV['PGPASSWORD'] %> 22 | host: <%= ENV['PGHOST'] %> 23 | port: <%= ENV['PGPORT'] %> 24 | 25 | test: 26 | <<: *default 27 | database: quantifier_test 28 | 29 | production: 30 | <<: *default 31 | database: quantifier_production 32 | 33 | 34 | # Schema search path. The server defaults to $user,public 35 | #schema_search_path: myapp,sharedapp,public 36 | 37 | # Minimum log levels, in increasing order: 38 | # debug5, debug4, debug3, debug2, debug1, 39 | # log, notice, warning, error, fatal, and panic 40 | # The server defaults to notice. 41 | #min_messages: warning 42 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.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 web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | # Run rails dev:cache to toggle caching. 17 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 18 | config.action_controller.perform_caching = true 19 | 20 | config.cache_store = :memory_store 21 | config.public_file_server.headers = { 22 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 23 | } 24 | else 25 | config.action_controller.perform_caching = false 26 | 27 | config.cache_store = :null_store 28 | end 29 | 30 | # Store uploaded files on the local file system (see config/storage.yml for options) 31 | config.active_storage.service = :local 32 | 33 | # Don't care if the mailer can't send. 34 | config.action_mailer.raise_delivery_errors = false 35 | 36 | config.action_mailer.perform_caching = false 37 | 38 | # Print deprecation notices to the Rails logger. 39 | config.active_support.deprecation = :log 40 | 41 | # Raise an error on page load if there are pending migrations. 42 | config.active_record.migration_error = :page_load 43 | 44 | # Highlight code that triggered database queries in logs. 45 | config.active_record.verbose_query_logs = true 46 | 47 | # Debug mode disables concatenation and preprocessing of assets. 48 | # This option may cause significant delays in view rendering with a large 49 | # number of complex assets. 50 | config.assets.debug = true 51 | 52 | # Suppress logger output for asset requests. 53 | config.assets.quiet = true 54 | 55 | # Raises error for missing translations 56 | # config.action_view.raise_on_missing_translations = true 57 | 58 | # Use an evented file watcher to asynchronously detect changes in source code, 59 | # routes, locales, etc. This feature depends on the listen gem. 60 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 61 | end 62 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 18 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 19 | # config.require_master_key = true 20 | 21 | # Disable serving static files from the `/public` folder by default since 22 | # Apache or NGINX already handles this. 23 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 24 | 25 | # Compress JavaScripts and CSS. 26 | config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = false 31 | 32 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 33 | 34 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 35 | # config.action_controller.asset_host = 'http://assets.example.com' 36 | 37 | # Specifies the header that your server uses for sending files. 38 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 39 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 40 | 41 | # Store uploaded files on the local file system (see config/storage.yml for options) 42 | config.active_storage.service = :local 43 | 44 | # Mount Action Cable outside main process or domain 45 | # config.action_cable.mount_path = nil 46 | # config.action_cable.url = 'wss://example.com/cable' 47 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 48 | 49 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 50 | config.force_ssl = true 51 | 52 | # Use the lowest log level to ensure availability of diagnostic information 53 | # when problems arise. 54 | config.log_level = :debug 55 | 56 | # Prepend all log lines with the following tags. 57 | config.log_tags = [ :request_id ] 58 | 59 | # Use a different cache store in production. 60 | # config.cache_store = :mem_cache_store 61 | 62 | # Use a real queuing backend for Active Job (and separate queues per environment) 63 | # config.active_job.queue_adapter = :resque 64 | # config.active_job.queue_name_prefix = "quantifier_#{Rails.env}" 65 | 66 | config.action_mailer.perform_caching = false 67 | 68 | # Ignore bad email addresses and do not raise email delivery errors. 69 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 70 | # config.action_mailer.raise_delivery_errors = false 71 | 72 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 73 | # the I18n.default_locale when a translation cannot be found). 74 | config.i18n.fallbacks = true 75 | 76 | # Send deprecation notices to registered listeners. 77 | config.active_support.deprecation = :notify 78 | 79 | # Use default logging formatter so that PID and timestamp are not suppressed. 80 | config.log_formatter = ::Logger::Formatter.new 81 | 82 | # Use a different logger for distributed setups. 83 | # require 'syslog/logger' 84 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 85 | 86 | if ENV["RAILS_LOG_TO_STDOUT"].present? 87 | logger = ActiveSupport::Logger.new(STDOUT) 88 | logger.formatter = config.log_formatter 89 | config.logger = ActiveSupport::TaggedLogging.new(logger) 90 | end 91 | 92 | # Do not dump schema after migrations. 93 | config.active_record.dump_schema_after_migration = false 94 | end 95 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Store uploaded files on the local file system in a temporary directory 32 | config.active_storage.service = :test 33 | 34 | config.action_mailer.perform_caching = false 35 | 36 | # Tell Action Mailer not to deliver emails to the real world. 37 | # The :test delivery method accumulates sent emails in the 38 | # ActionMailer::Base.deliveries array. 39 | config.action_mailer.delivery_method = :test 40 | 41 | # Print deprecation notices to the stderr. 42 | config.active_support.deprecation = :stderr 43 | 44 | # Raises error for missing translations 45 | # config.action_view.raise_on_missing_translations = true 46 | end 47 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | ApplicationController.renderer.defaults.merge!( 4 | http_host: 'beemind.me', 5 | https: true 6 | ) 7 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /config/initializers/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/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | 19 | # If you are using UJS then enable automatic nonce generation 20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 21 | 22 | # Report CSP violations to a specified URI 23 | # For further information see the following documentation: 24 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 25 | # Rails.application.config.content_security_policy_report_only = true 26 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults_5_2.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.2 upgrade. 4 | # 5 | # Once upgraded flip defaults one by one to migrate to the new default. 6 | # 7 | # Read the Guide for Upgrading Ruby on Rails for more info on each option. 8 | 9 | # Make Active Record use stable #cache_key alongside new #cache_version method. 10 | # This is needed for recyclable cache keys. 11 | # Rails.application.config.active_record.cache_versioning = true 12 | 13 | # Use AES-256-GCM authenticated encryption for encrypted cookies. 14 | # Also, embed cookie expiry in signed or encrypted cookies for increased security. 15 | # 16 | # This option is not backwards compatible with earlier Rails versions. 17 | # It's best enabled when your entire app is migrated and stable on 5.2. 18 | # 19 | # Existing cookies will be converted on read then written with the new scheme. 20 | # Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true 21 | 22 | # Use AES-256-GCM authenticated encryption as default cipher for encrypting messages 23 | # instead of AES-256-CBC, when use_authenticated_message_encryption is set to true. 24 | # Rails.application.config.active_support.use_authenticated_message_encryption = true 25 | 26 | # Add default protection from forgery to ActionController::Base instead of in 27 | # ApplicationController. 28 | # Rails.application.config.action_controller.default_protect_from_forgery = true 29 | 30 | # Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and 31 | # 'f' after migrating old data. 32 | # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true 33 | 34 | # Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. 35 | # Rails.application.config.active_support.use_sha1_digests = true 36 | 37 | # Make `form_with` generate id attributes for any generated HTML tags. 38 | # Rails.application.config.action_view.form_with_generates_ids = true 39 | -------------------------------------------------------------------------------- /config/initializers/omniauth.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.middleware.use OmniAuth::Builder do 2 | provider :beeminder, 3 | Rails.application.secrets.beeminder_provider_key, 4 | Rails.application.secrets.beeminder_provider_secret 5 | 6 | provider :pocket, 7 | Rails.application.secrets.pocket_provider_key 8 | 9 | provider :trello, 10 | Rails.application.secrets.trello_provider_key, 11 | Rails.application.secrets.trello_provider_secret, 12 | app_name: "Beemind.me", 13 | scope: "read", 14 | expiration: "never" 15 | 16 | provider :google_oauth2, 17 | Rails.application.secrets.google_provider_key, 18 | Rails.application.secrets.google_provider_secret, 19 | scope: %w[ 20 | email 21 | https://www.googleapis.com/auth/fitness.activity.read 22 | https://www.googleapis.com/auth/fitness.nutrition.read 23 | https://www.googleapis.com/auth/fitness.body.read 24 | https://www.googleapis.com/auth/fitness.blood_glucose.read 25 | https://www.googleapis.com/auth/fitness.blood_pressure.read 26 | ].join(","), 27 | access_type: "offline", 28 | name: "googlefit", 29 | prompt: "consent" 30 | 31 | 32 | end 33 | -------------------------------------------------------------------------------- /config/initializers/per_form_csrf_tokens.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Enable per-form CSRF tokens. 4 | Rails.application.config.action_controller.per_form_csrf_tokens = true 5 | -------------------------------------------------------------------------------- /config/initializers/providers.rb: -------------------------------------------------------------------------------- 1 | PROVIDERS = %i( 2 | googlefit trello pocket beeminder bcycle stackoverflow 3 | ).map do |p_key| 4 | adapter = "#{p_key}_adapter".camelize.constantize 5 | [p_key, Provider.new(p_key, adapter)] 6 | end.to_h.with_indifferent_access 7 | PROVIDERS.values.each(&:load_metrics) 8 | -------------------------------------------------------------------------------- /config/initializers/request_forgery_protection.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Enable origin-checking CSRF mitigation. 4 | Rails.application.config.action_controller.forgery_protection_origin_check = true 5 | -------------------------------------------------------------------------------- /config/initializers/rollbar.rb: -------------------------------------------------------------------------------- 1 | require 'rollbar/rails' 2 | Rollbar.configure do |config| 3 | # Without configuration, Rollbar is enabled in all environments. 4 | # To disable in specific environments, set config.enabled=false. 5 | 6 | config.access_token = Rails.application.secrets.rollbar_access_token 7 | 8 | # Here we'll disable in 'test': 9 | unless Rails.env.production? 10 | config.enabled = false 11 | end 12 | 13 | # By default, Rollbar will try to call the `current_user` controller method 14 | # to fetch the logged-in user object, and then call that object's `id`, 15 | # `username`, and `email` methods to fetch those properties. To customize: 16 | # config.person_method = "my_current_user" 17 | # config.person_id_method = "my_id" 18 | # config.person_username_method = "my_username" 19 | # config.person_email_method = "my_email" 20 | 21 | # If you want to attach custom data to all exception and message reports, 22 | # provide a lambda like the following. It should return a hash. 23 | # config.custom_data_method = lambda { {:some_key => "some_value" } } 24 | 25 | # Add exception class names to the exception_level_filters hash to 26 | # change the level that exception is reported at. Note that if an exception 27 | # has already been reported and logged the level will need to be changed 28 | # via the rollbar interface. 29 | # Valid levels: 'critical', 'error', 'warning', 'info', 'debug', 'ignore' 30 | # 'ignore' will cause the exception to not be reported at all. 31 | # config.exception_level_filters.merge!('MyCriticalException' => 'critical') 32 | # 33 | # You can also specify a callable, which will be called with the exception instance. 34 | # config.exception_level_filters.merge!('MyCriticalException' => lambda { |e| 'critical' }) 35 | 36 | # Enable asynchronous reporting (uses girl_friday or Threading if girl_friday 37 | # is not installed) 38 | # config.use_async = true 39 | # Supply your own async handler: 40 | # config.async_handler = Proc.new { |payload| 41 | # Thread.new { Rollbar.process_payload_safely(payload) } 42 | # } 43 | 44 | # Enable asynchronous reporting (using sucker_punch) 45 | # config.use_sucker_punch 46 | 47 | # Enable delayed reporting (using Sidekiq) 48 | # config.use_sidekiq 49 | # You can supply custom Sidekiq options: 50 | # config.use_sidekiq 'queue' => 'my_queue' 51 | end 52 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_quantifier_session' 4 | -------------------------------------------------------------------------------- /config/initializers/sidekiq.rb: -------------------------------------------------------------------------------- 1 | Sidekiq::Logging.logger = Rails.logger 2 | -------------------------------------------------------------------------------- /config/initializers/simple_form.rb: -------------------------------------------------------------------------------- 1 | # Use this setup block to configure all options available in SimpleForm. 2 | SimpleForm.setup do |config| 3 | # Wrappers are used by the form builder to generate a 4 | # complete input. You can remove any component from the 5 | # wrapper, change the order or even add your own to the 6 | # stack. The options given below are used to wrap the 7 | # whole input. 8 | config.wrappers :default, class: :input, 9 | hint_class: :field_with_hint, error_class: :field_with_errors do |b| 10 | ## Extensions enabled by default 11 | # Any of these extensions can be disabled for a 12 | # given input by passing: `f.input EXTENSION_NAME => false`. 13 | # You can make any of these extensions optional by 14 | # renaming `b.use` to `b.optional`. 15 | 16 | # Determines whether to use HTML5 (:email, :url, ...) 17 | # and required attributes 18 | b.use :html5 19 | 20 | # Calculates placeholders automatically from I18n 21 | # You can also pass a string as f.input placeholder: "Placeholder" 22 | b.use :placeholder 23 | 24 | ## Optional extensions 25 | # They are disabled unless you pass `f.input EXTENSION_NAME => :lookup` 26 | # to the input. If so, they will retrieve the values from the model 27 | # if any exists. If you want to enable the lookup for any of those 28 | # extensions by default, you can change `b.optional` to `b.use`. 29 | 30 | # Calculates maxlength from length validations for string inputs 31 | b.optional :maxlength 32 | 33 | # Calculates pattern from format validations for string inputs 34 | b.optional :pattern 35 | 36 | # Calculates min and max from length validations for numeric inputs 37 | b.optional :min_max 38 | 39 | # Calculates readonly automatically from readonly attributes 40 | b.optional :readonly 41 | 42 | ## Inputs 43 | b.use :label_input 44 | b.use :hint, wrap_with: { tag: :span, class: :hint } 45 | b.use :error, wrap_with: { tag: :span, class: :error } 46 | end 47 | 48 | # The default wrapper to be used by the FormBuilder. 49 | config.default_wrapper = :default 50 | 51 | # Define the way to render check boxes / radio buttons with labels. 52 | # Defaults to :nested for bootstrap config. 53 | # inline: input + label 54 | # nested: label > input 55 | config.boolean_style = :nested 56 | 57 | # Default class for buttons 58 | config.button_class = "btn" 59 | 60 | # Method used to tidy up errors. Specify any Rails Array method. 61 | # :first lists the first message for each field. 62 | # Use :to_sentence to list all errors for each field. 63 | # config.error_method = :first 64 | 65 | # Default tag used for error notification helper. 66 | config.error_notification_tag = :div 67 | 68 | # CSS class to add for error notification helper. 69 | config.error_notification_class = "alert alert-error" 70 | 71 | # ID to add for error notification helper. 72 | # config.error_notification_id = nil 73 | 74 | # Series of attempts to detect a default label method for collection. 75 | # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] 76 | 77 | # Series of attempts to detect a default value method for collection. 78 | # config.collection_value_methods = [ :id, :to_s ] 79 | 80 | # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none. 81 | # config.collection_wrapper_tag = nil 82 | 83 | # You can define the class to use on all collection wrappers. Defaulting to none. 84 | # config.collection_wrapper_class = nil 85 | 86 | # You can wrap each item in a collection of radio/check boxes with a tag, 87 | # defaulting to :span. Please note that when using :boolean_style = :nested, 88 | # SimpleForm will force this option to be a label. 89 | # config.item_wrapper_tag = :span 90 | 91 | # You can define a class to use in all item wrappers. Defaulting to none. 92 | # config.item_wrapper_class = nil 93 | 94 | # How the label text should be generated altogether with the required text. 95 | # config.label_text = lambda { |label, required| "#{required} #{label}" } 96 | 97 | # You can define the class to use on all labels. Default is nil. 98 | config.label_class = "control-label" 99 | 100 | # You can define the class to use on all forms. Default is simple_form. 101 | # config.form_class = :simple_form 102 | 103 | # You can define which elements should obtain additional classes 104 | # config.generate_additional_classes_for = [:wrapper, :label, :input] 105 | 106 | # Whether attributes are required by default (or not). Default is true. 107 | # config.required_by_default = true 108 | 109 | # Tell browsers whether to use the native HTML5 validations (novalidate form option). 110 | # These validations are enabled in SimpleForm's internal config but disabled by default 111 | # in this configuration, which is recommended due to some quirks from different browsers. 112 | # To stop SimpleForm from generating the novalidate option, enabling the HTML5 validations, 113 | # change this configuration to true. 114 | config.browser_validations = false 115 | 116 | # Collection of methods to detect if a file type was given. 117 | # config.file_methods = [ :mounted_as, :file?, :public_filename ] 118 | 119 | # Custom mappings for input types. This should be a hash containing a regexp 120 | # to match as key, and the input type that will be used when the field name 121 | # matches the regexp as value. 122 | # config.input_mappings = { /count/ => :integer } 123 | 124 | # Custom wrappers for input types. This should be a hash containing an input 125 | # type as key and the wrapper that will be used for all inputs with specified type. 126 | # config.wrapper_mappings = { string: :prepend } 127 | 128 | # Default priority for time_zone inputs. 129 | # config.time_zone_priority = nil 130 | 131 | # Default priority for country inputs. 132 | # config.country_priority = nil 133 | 134 | # When false, do not use translations for labels. 135 | # config.translate_labels = true 136 | 137 | # Automatically discover new inputs in Rails' autoload path. 138 | # config.inputs_discovery = true 139 | 140 | # Cache SimpleForm inputs discovery 141 | # config.cache_discovery = !Rails.env.development? 142 | 143 | # Default class for inputs 144 | # config.input_class = nil 145 | end 146 | -------------------------------------------------------------------------------- /config/initializers/simple_form_bootstrap.rb: -------------------------------------------------------------------------------- 1 | # Use this setup block to configure all options available in SimpleForm. 2 | SimpleForm.setup do |config| 3 | config.wrappers :bootstrap, tag: "div", class: "control-group", error_class: "error" do |b| 4 | b.use :html5 5 | b.use :placeholder 6 | b.use :label 7 | b.wrapper tag: "div", class: "controls" do |ba| 8 | ba.use :input 9 | ba.use :error, wrap_with: { tag: "span", class: "help-inline" } 10 | ba.use :hint, wrap_with: { tag: "p", class: "help-block" } 11 | end 12 | end 13 | 14 | config.wrappers :prepend, tag: "div", class: "control-group", error_class: "error" do |b| 15 | b.use :html5 16 | b.use :placeholder 17 | b.use :label 18 | b.wrapper tag: "div", class: "controls" do |input| 19 | input.wrapper tag: "div", class: "input-prepend" do |prepend| 20 | prepend.use :input 21 | end 22 | input.use :hint, wrap_with: { tag: "span", class: "help-block" } 23 | input.use :error, wrap_with: { tag: "span", class: "help-inline" } 24 | end 25 | end 26 | 27 | config.wrappers :append, tag: "div", class: "control-group", error_class: "error" do |b| 28 | b.use :html5 29 | b.use :placeholder 30 | b.use :label 31 | b.wrapper tag: "div", class: "controls" do |input| 32 | input.wrapper tag: "div", class: "input-append" do |append| 33 | append.use :input 34 | end 35 | input.use :hint, wrap_with: { tag: "span", class: "help-block" } 36 | input.use :error, wrap_with: { tag: "span", class: "help-inline" } 37 | end 38 | end 39 | 40 | # Wrappers for forms and inputs using the Twitter Bootstrap toolkit. 41 | # Check the Bootstrap docs (http://twitter.github.com/bootstrap) 42 | # to learn about the different styles for forms and inputs, 43 | # buttons and other elements. 44 | config.default_wrapper = :bootstrap 45 | end 46 | -------------------------------------------------------------------------------- /config/initializers/utc_constant.rb: -------------------------------------------------------------------------------- 1 | UTC = ActiveSupport::TimeZone.new("UTC").freeze 2 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /config/locales/simple_form.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | simple_form: 3 | "yes": 'Yes' 4 | "no": 'No' 5 | required: 6 | text: 'required' 7 | mark: '*' 8 | # You can uncomment the line below if you need to overwrite the whole required html. 9 | # When using html, text and mark won't be used. 10 | # html: '*' 11 | error_notification: 12 | default_message: "Please review the problems below:" 13 | # Labels and hints examples 14 | # labels: 15 | # defaults: 16 | # password: 'Password' 17 | # user: 18 | # new: 19 | # email: 'E-mail to sign in.' 20 | # edit: 21 | # email: 'E-mail.' 22 | # hints: 23 | # defaults: 24 | # username: 'User name to sign in.' 25 | # password: 'No special characters, please.' 26 | 27 | -------------------------------------------------------------------------------- /config/newrelic.yml: -------------------------------------------------------------------------------- 1 | # 2 | # This file configures the New Relic Agent. New Relic monitors Ruby, Java, 3 | # .NET, PHP, Python and Node applications with deep visibility and low 4 | # overhead. For more information, visit www.newrelic.com. 5 | # 6 | # Generated October 22, 2014 7 | # 8 | # This configuration file is custom generated for Yoram Tsubery 9 | 10 | 11 | # Here are the settings that are common to all environments 12 | common: &default_settings 13 | # ============================== LICENSE KEY =============================== 14 | 15 | # You must specify the license key associated with your New Relic 16 | # account. This key binds your Agent's data to your account in the 17 | # New Relic service. 18 | license_key: 'cb10a1dde657898f6845787e886f84797b072ed6' 19 | 20 | # Agent Enabled (Ruby/Rails Only) 21 | # Use this setting to force the agent to run or not run. 22 | # Default is 'auto' which means the agent will install and run only 23 | # if a valid dispatcher such as Mongrel is running. This prevents 24 | # it from running with Rake or the console. Set to false to 25 | # completely turn the agent off regardless of the other settings. 26 | # Valid values are true, false and auto. 27 | # 28 | # agent_enabled: auto 29 | 30 | # Application Name Set this to be the name of your application as 31 | # you'd like it show up in New Relic. The service will then auto-map 32 | # instances of your application into an "application" on your 33 | # dashboard page. If you want to map this instance into multiple 34 | # apps, like "AJAX Requests" and "All UI" then specify a semicolon 35 | # separated list of up to three distinct names, or a yaml list. 36 | # Defaults to the capitalized RAILS_ENV or RACK_ENV (i.e., 37 | # Production, Staging, etc) 38 | # 39 | # Example: 40 | # 41 | # app_name: 42 | # - Ajax Service 43 | # - All Services 44 | # 45 | # Caution: If you change this name, a new application will appear in the New 46 | # Relic user interface with the new name, and data will stop reporting to the 47 | # app with the old name. 48 | # 49 | # See https://newrelic.com/docs/site/renaming-applications for more details 50 | # on renaming your New Relic applications. 51 | # 52 | app_name: My Application 53 | 54 | # When "true", the agent collects performance data about your 55 | # application and reports this data to the New Relic service at 56 | # newrelic.com. This global switch is normally overridden for each 57 | # environment below. (formerly called 'enabled') 58 | monitor_mode: true 59 | 60 | # Developer mode should be off in every environment but 61 | # development as it has very high overhead in memory. 62 | developer_mode: false 63 | 64 | # The newrelic agent generates its own log file to keep its logging 65 | # information separate from that of your application. Specify its 66 | # log level here. 67 | log_level: info 68 | 69 | # Optionally set the path to the log file This is expanded from the 70 | # root directory (may be relative or absolute, e.g. 'log/' or 71 | # '/var/log/') The agent will attempt to create this directory if it 72 | # does not exist. 73 | # log_file_path: 'log' 74 | 75 | # Optionally set the name of the log file, defaults to 'newrelic_agent.log' 76 | # log_file_name: 'newrelic_agent.log' 77 | 78 | # The newrelic agent communicates with the service via https by default. This 79 | # prevents eavesdropping on the performance metrics transmitted by the agent. 80 | # The encryption required by SSL introduces a nominal amount of CPU overhead, 81 | # which is performed asynchronously in a background thread. If you'd prefer 82 | # to send your metrics over http uncomment the following line. 83 | # ssl: false 84 | 85 | #============================== Browser Monitoring =============================== 86 | # New Relic Real User Monitoring gives you insight into the performance real users are 87 | # experiencing with your website. This is accomplished by measuring the time it takes for 88 | # your users' browsers to download and render your web pages by injecting a small amount 89 | # of JavaScript code into the header and footer of each page. 90 | browser_monitoring: 91 | # By default the agent automatically injects the monitoring JavaScript 92 | # into web pages. Set this attribute to false to turn off this behavior. 93 | auto_instrument: true 94 | 95 | # Proxy settings for connecting to the New Relic server. 96 | # 97 | # If a proxy is used, the host setting is required. Other settings 98 | # are optional. Default port is 8080. 99 | # 100 | # proxy_host: hostname 101 | # proxy_port: 8080 102 | # proxy_user: 103 | # proxy_pass: 104 | 105 | # The agent can optionally log all data it sends to New Relic servers to a 106 | # separate log file for human inspection and auditing purposes. To enable this 107 | # feature, change 'enabled' below to true. 108 | # See: https://newrelic.com/docs/ruby/audit-log 109 | audit_log: 110 | enabled: false 111 | 112 | # Tells transaction tracer and error collector (when enabled) 113 | # whether or not to capture HTTP params. When true, frameworks can 114 | # exclude HTTP parameters from being captured. 115 | # Rails: the RoR filter_parameter_logging excludes parameters 116 | # Java: create a config setting called "ignored_params" and set it to 117 | # a comma separated list of HTTP parameter names. 118 | # ex: ignored_params: credit_card, ssn, password 119 | capture_params: false 120 | 121 | # Transaction tracer captures deep information about slow 122 | # transactions and sends this to the New Relic service once a 123 | # minute. Included in the transaction is the exact call sequence of 124 | # the transactions including any SQL statements issued. 125 | transaction_tracer: 126 | 127 | # Transaction tracer is enabled by default. Set this to false to 128 | # turn it off. This feature is only available at the Professional 129 | # and above product levels. 130 | enabled: true 131 | 132 | # Threshold in seconds for when to collect a transaction 133 | # trace. When the response time of a controller action exceeds 134 | # this threshold, a transaction trace will be recorded and sent to 135 | # New Relic. Valid values are any float value, or (default) "apdex_f", 136 | # which will use the threshold for an dissatisfying Apdex 137 | # controller action - four times the Apdex T value. 138 | transaction_threshold: apdex_f 139 | 140 | # When transaction tracer is on, SQL statements can optionally be 141 | # recorded. The recorder has three modes, "off" which sends no 142 | # SQL, "raw" which sends the SQL statement in its original form, 143 | # and "obfuscated", which strips out numeric and string literals. 144 | record_sql: obfuscated 145 | 146 | # Threshold in seconds for when to collect stack trace for a SQL 147 | # call. In other words, when SQL statements exceed this threshold, 148 | # then capture and send to New Relic the current stack trace. This is 149 | # helpful for pinpointing where long SQL calls originate from. 150 | stack_trace_threshold: 0.500 151 | 152 | # Determines whether the agent will capture query plans for slow 153 | # SQL queries. Only supported in mysql and postgres. Should be 154 | # set to false when using other adapters. 155 | # explain_enabled: true 156 | 157 | # Threshold for query execution time below which query plans will 158 | # not be captured. Relevant only when `explain_enabled` is true. 159 | # explain_threshold: 0.5 160 | 161 | # Error collector captures information about uncaught exceptions and 162 | # sends them to New Relic for viewing 163 | error_collector: 164 | 165 | # Error collector is enabled by default. Set this to false to turn 166 | # it off. This feature is only available at the Professional and above 167 | # product levels. 168 | enabled: true 169 | 170 | # To stop specific errors from reporting to New Relic, set this property 171 | # to comma-separated values. Default is to ignore routing errors, 172 | # which are how 404's get triggered. 173 | ignore_errors: "ActionController::RoutingError,Sinatra::NotFound" 174 | 175 | # If you're interested in capturing memcache keys as though they 176 | # were SQL uncomment this flag. Note that this does increase 177 | # overhead slightly on every memcached call, and can have security 178 | # implications if your memcached keys are sensitive 179 | # capture_memcache_keys: true 180 | 181 | # Application Environments 182 | # ------------------------------------------ 183 | # Environment-specific settings are in this section. 184 | # For Rails applications, RAILS_ENV is used to determine the environment. 185 | # For Java applications, pass -Dnewrelic.environment to set 186 | # the environment. 187 | 188 | # NOTE if your application has other named environments, you should 189 | # provide newrelic configuration settings for these environments here. 190 | 191 | development: 192 | <<: *default_settings 193 | # Turn on communication to New Relic service in development mode 194 | monitor_mode: true 195 | app_name: My Application (Development) 196 | 197 | # Rails Only - when running in Developer Mode, the New Relic Agent will 198 | # present performance information on the last 100 transactions you have 199 | # executed since starting the mongrel. 200 | # NOTE: There is substantial overhead when running in developer mode. 201 | # Do not use for production or load testing. 202 | developer_mode: true 203 | 204 | test: 205 | <<: *default_settings 206 | # It almost never makes sense to turn on the agent when running 207 | # unit, functional or integration tests or the like. 208 | monitor_mode: false 209 | 210 | # Turn on the agent in production for 24x7 monitoring. NewRelic 211 | # testing shows an average performance impact of < 5 ms per 212 | # transaction, you can leave this on all the time without 213 | # incurring any user-visible performance degradation. 214 | production: 215 | <<: *default_settings 216 | monitor_mode: true 217 | 218 | # Many applications have a staging environment which behaves 219 | # identically to production. Support for that environment is provided 220 | # here. By default, the staging environment has the agent turned on. 221 | staging: 222 | <<: *default_settings 223 | monitor_mode: true 224 | app_name: My Application (Staging) 225 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum, this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # The code in the `on_worker_boot` will be called if you are using 36 | # clustered mode by specifying a number of `workers`. After each worker 37 | # process is booted this block will be run, if you are using `preload_app!` 38 | # option you will want to use this block to reconnect to any threads 39 | # or connections that may have been created at application boot, Ruby 40 | # cannot share connections between processes. 41 | # 42 | # on_worker_boot do 43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 44 | # end 45 | 46 | # Allow puma to be restarted by `rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root to: "main#index" 3 | get "/auth/:provider/callback" => "sessions#create" 4 | get "/signin" => "sessions#new", :as => :signin 5 | get "/signout" => "sessions#destroy", :as => :signout 6 | get "/auth/failure" => "sessions#failure" 7 | 8 | resources :goals, 9 | only: %i(destroy), 10 | constraints: { id: /\d+/} do 11 | 12 | collection do 13 | metric_exists = lambda do |request| 14 | name = request[:provider_name] 15 | PROVIDERS[name]&.find_metric(request[:metric_key]) 16 | end 17 | 18 | get ':provider_name/:metric_key', 19 | constraints: metric_exists, 20 | action: :edit, 21 | as: :edit 22 | post ':provider_name/:metric_key', 23 | constraints: metric_exists, 24 | action: :upsert, 25 | as: :upsert 26 | 27 | post 'reload', action: :reload, as: :reload 28 | end 29 | end 30 | resources :credentials, 31 | except: %(index show) 32 | 33 | post 'callback/reload_goal', controller: :callback, action: :reload_slug 34 | end 35 | -------------------------------------------------------------------------------- /config/schedule.rb: -------------------------------------------------------------------------------- 1 | require "whenever" 2 | 3 | set :output, "#{path}/log/cron_log.log" 4 | set :environment, @environment 5 | 6 | every '55 * * * *' do 7 | runner "BeeminderWorker.perform_async" 8 | end 9 | 10 | every 1.day do 11 | runner "BackupWorker.perform_async" 12 | end 13 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: &default 14 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 15 | google_provider_key: <%= ENV["GOOGLE_PROVIDER_KEY"] %> 16 | google_provider_secret: <%= ENV["GOOGLE_PROVIDER_SECRET"] %> 17 | trello_provider_key: <%= ENV["TRELLO_PROVIDER_KEY"] %> 18 | trello_provider_secret: <%= ENV["TRELLO_PROVIDER_SECRET"] %> 19 | beeminder_provider_key: <%= ENV["BEEMINDER_PROVIDER_KEY"] %> 20 | beeminder_provider_secret: <%= ENV["BEEMINDER_PROVIDER_SECRET"] %> 21 | email_provider_username: <%= ENV["GMAIL_USERNAME"] %> 22 | email_provider_password: <%= ENV["GMAIL_PASSWORD"] %> 23 | pocket_provider_key: <%= ENV["POCKET_PROVIDER_KEY"] %> 24 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 25 | domain_name: <%= ENV["DOMAIN_NAME"] %> 26 | rollbar_access_token: <%= ENV['ROLLBAR_ACCESS_TOKEN'] %> 27 | dumper_app_key: <%= ENV['DUMPER_APP_KEY'] %> 28 | 29 | test: 30 | <<: *default 31 | 32 | production: &development 33 | <<: *default 34 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /db/migrate/20140714204840_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table :users, id: false do |t| 4 | t.string :beeminder_token, null: false 5 | t.string :beeminder_user_id, null: false, primary: true, references: nil 6 | 7 | t.timestamps 8 | end 9 | add_index :users, :beeminder_user_id, unique: true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20140729165051_create_providers.rb: -------------------------------------------------------------------------------- 1 | class CreateProviders < ActiveRecord::Migration 2 | def change 3 | create_table :providers do |t| 4 | t.string :beeminder_user_id, null: false, references: nil 5 | t.string :name, null: false 6 | t.string :uid, default: "", null: false 7 | t.json :info, default: "{}", null: false 8 | t.json :credentials, default: "{}", null: false 9 | t.json :extra, default: "{}", null: false 10 | t.timestamps 11 | end 12 | add_foreign_key :providers, :users, column: :beeminder_user_id, primary_key: :beeminder_user_id 13 | add_index :providers, [:name, :uid], unique: true 14 | add_index :providers, [:name, :beeminder_user_id], unique: true 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20150719185636_create_goals.rb: -------------------------------------------------------------------------------- 1 | class CreateGoals < ActiveRecord::Migration 2 | def change 3 | create_table :goals do |t| 4 | t.references :provider, null: false 5 | t.string :slug, null: false 6 | t.float :last_value 7 | end 8 | add_index :goals, [:slug, :provider_id], unique: true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20150721223451_add_params_to_goals.rb: -------------------------------------------------------------------------------- 1 | class AddParamsToGoals < ActiveRecord::Migration 2 | def change 3 | add_column :goals, :params, :json, default: {}, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150907205208_rename_providers_to_credentials.rb: -------------------------------------------------------------------------------- 1 | class RenameProvidersToCredentials < ActiveRecord::Migration 2 | def change 3 | rename_table :providers, :credentials 4 | rename_column :goals, :provider_id, :credential_id 5 | rename_column :credentials, :name, :provider_name 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20150908234553_add_metric_key_to_goal.rb: -------------------------------------------------------------------------------- 1 | class AddMetricKeyToGoal < ActiveRecord::Migration 2 | def change 3 | add_column :goals, :metric_key, :string 4 | Goal.find_each do |goal| 5 | p goal.credential_id 6 | goal.update metric_key: goal.credential.provider.metrics.first.key 7 | end 8 | change_column :goals, :metric_key, :string, null: false 9 | add_index :goals, :metric_key 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20150909225051_create_scores.rb: -------------------------------------------------------------------------------- 1 | class CreateScores < ActiveRecord::Migration 2 | def change 3 | create_table :scores do |t| 4 | t.float :value, null: false 5 | t.datetime :timestamp, null: false 6 | t.string :datapoint_id, foreign_key: false 7 | t.references :goal, index: true, foreign_key: true 8 | t.timestamps 9 | end 10 | add_index :scores, [:goal_id, :timestamp], unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20150926194732_change_score_datpoint_id_to_unique.rb: -------------------------------------------------------------------------------- 1 | class ChangeScoreDatpointIdToUnique < ActiveRecord::Migration 2 | def change 3 | add_column :scores, :unique, :boolean 4 | Score.update_all(unique: false) 5 | Score.where.not(datapoint_id: nil).update_all(unique: true) 6 | remove_column :scores, :datapoint_id 7 | change_column_null :scores, :unique, true 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20150926210204_add_status_flag_to_goal.rb: -------------------------------------------------------------------------------- 1 | class AddStatusFlagToGoal < ActiveRecord::Migration 2 | def change 3 | add_column :goals, :active, :boolean, default: true, null: false 4 | add_column :goals, :fail_count, :integer, default: 0, null: false 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20160316220158_remove_beeminder_token_from_users_table.rb: -------------------------------------------------------------------------------- 1 | class RemoveBeeminderTokenFromUsersTable < ActiveRecord::Migration 2 | def change 3 | User.all.each do |u| 4 | Credential.create!( 5 | beeminder_user_id: u.beeminder_user_id, 6 | provider_name: :beeminder, 7 | uid: u.beeminder_user_id, 8 | credentials: { 9 | "token" => u.beeminder_token 10 | } 11 | ) 12 | end 13 | remove_column :users, :beeminder_token 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20160519212602_add_password_to_credential.rb: -------------------------------------------------------------------------------- 1 | class AddPasswordToCredential < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :credentials, :password, :string, null: false, default: "" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170125204752_add_timestamps_to_goals.rb: -------------------------------------------------------------------------------- 1 | class AddTimestampsToGoals < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :goals, :created_at, :datetime 4 | add_column :goals, :updated_at, :datetime 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/structure.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 9.5.2 6 | -- Dumped by pg_dump version 9.5.2 7 | 8 | SET statement_timeout = 0; 9 | SET lock_timeout = 0; 10 | SET client_encoding = 'UTF8'; 11 | SET standard_conforming_strings = on; 12 | SET check_function_bodies = false; 13 | SET client_min_messages = warning; 14 | SET row_security = off; 15 | 16 | -- 17 | -- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: - 18 | -- 19 | 20 | CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; 21 | 22 | 23 | -- 24 | -- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: - 25 | -- 26 | 27 | COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; 28 | 29 | 30 | SET search_path = public, pg_catalog; 31 | 32 | SET default_tablespace = ''; 33 | 34 | SET default_with_oids = false; 35 | 36 | -- 37 | -- Name: ar_internal_metadata; Type: TABLE; Schema: public; Owner: - 38 | -- 39 | 40 | CREATE TABLE ar_internal_metadata ( 41 | key character varying NOT NULL, 42 | value character varying, 43 | created_at timestamp without time zone NOT NULL, 44 | updated_at timestamp without time zone NOT NULL 45 | ); 46 | 47 | 48 | -- 49 | -- Name: credentials; Type: TABLE; Schema: public; Owner: - 50 | -- 51 | 52 | CREATE TABLE credentials ( 53 | id integer NOT NULL, 54 | beeminder_user_id character varying NOT NULL, 55 | provider_name character varying NOT NULL, 56 | uid character varying DEFAULT ''::character varying NOT NULL, 57 | info json DEFAULT '{}'::json NOT NULL, 58 | credentials json DEFAULT '{}'::json NOT NULL, 59 | extra json DEFAULT '{}'::json NOT NULL, 60 | created_at timestamp without time zone, 61 | updated_at timestamp without time zone, 62 | password character varying DEFAULT ''::character varying NOT NULL 63 | ); 64 | 65 | 66 | -- 67 | -- Name: credentials_id_seq; Type: SEQUENCE; Schema: public; Owner: - 68 | -- 69 | 70 | CREATE SEQUENCE credentials_id_seq 71 | START WITH 1 72 | INCREMENT BY 1 73 | NO MINVALUE 74 | NO MAXVALUE 75 | CACHE 1; 76 | 77 | 78 | -- 79 | -- Name: credentials_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 80 | -- 81 | 82 | ALTER SEQUENCE credentials_id_seq OWNED BY credentials.id; 83 | 84 | 85 | -- 86 | -- Name: goals; Type: TABLE; Schema: public; Owner: - 87 | -- 88 | 89 | CREATE TABLE goals ( 90 | id integer NOT NULL, 91 | credential_id integer NOT NULL, 92 | slug character varying NOT NULL, 93 | last_value double precision, 94 | params json DEFAULT '{}'::json NOT NULL, 95 | metric_key character varying NOT NULL, 96 | active boolean DEFAULT true NOT NULL, 97 | fail_count integer DEFAULT 0 NOT NULL, 98 | created_at timestamp without time zone, 99 | updated_at timestamp without time zone 100 | ); 101 | 102 | 103 | -- 104 | -- Name: goals_id_seq; Type: SEQUENCE; Schema: public; Owner: - 105 | -- 106 | 107 | CREATE SEQUENCE goals_id_seq 108 | START WITH 1 109 | INCREMENT BY 1 110 | NO MINVALUE 111 | NO MAXVALUE 112 | CACHE 1; 113 | 114 | 115 | -- 116 | -- Name: goals_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 117 | -- 118 | 119 | ALTER SEQUENCE goals_id_seq OWNED BY goals.id; 120 | 121 | 122 | -- 123 | -- Name: schema_migrations; Type: TABLE; Schema: public; Owner: - 124 | -- 125 | 126 | CREATE TABLE schema_migrations ( 127 | version character varying NOT NULL 128 | ); 129 | 130 | 131 | -- 132 | -- Name: scores; Type: TABLE; Schema: public; Owner: - 133 | -- 134 | 135 | CREATE TABLE scores ( 136 | id integer NOT NULL, 137 | value double precision NOT NULL, 138 | "timestamp" timestamp without time zone NOT NULL, 139 | goal_id integer, 140 | created_at timestamp without time zone, 141 | updated_at timestamp without time zone, 142 | "unique" boolean 143 | ); 144 | 145 | 146 | -- 147 | -- Name: scores_id_seq; Type: SEQUENCE; Schema: public; Owner: - 148 | -- 149 | 150 | CREATE SEQUENCE scores_id_seq 151 | START WITH 1 152 | INCREMENT BY 1 153 | NO MINVALUE 154 | NO MAXVALUE 155 | CACHE 1; 156 | 157 | 158 | -- 159 | -- Name: scores_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 160 | -- 161 | 162 | ALTER SEQUENCE scores_id_seq OWNED BY scores.id; 163 | 164 | 165 | -- 166 | -- Name: users; Type: TABLE; Schema: public; Owner: - 167 | -- 168 | 169 | CREATE TABLE users ( 170 | beeminder_user_id character varying NOT NULL, 171 | created_at timestamp without time zone, 172 | updated_at timestamp without time zone 173 | ); 174 | 175 | 176 | -- 177 | -- Name: id; Type: DEFAULT; Schema: public; Owner: - 178 | -- 179 | 180 | ALTER TABLE ONLY credentials ALTER COLUMN id SET DEFAULT nextval('credentials_id_seq'::regclass); 181 | 182 | 183 | -- 184 | -- Name: id; Type: DEFAULT; Schema: public; Owner: - 185 | -- 186 | 187 | ALTER TABLE ONLY goals ALTER COLUMN id SET DEFAULT nextval('goals_id_seq'::regclass); 188 | 189 | 190 | -- 191 | -- Name: id; Type: DEFAULT; Schema: public; Owner: - 192 | -- 193 | 194 | ALTER TABLE ONLY scores ALTER COLUMN id SET DEFAULT nextval('scores_id_seq'::regclass); 195 | 196 | 197 | -- 198 | -- Name: ar_internal_metadata_pkey; Type: CONSTRAINT; Schema: public; Owner: - 199 | -- 200 | 201 | ALTER TABLE ONLY ar_internal_metadata 202 | ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key); 203 | 204 | 205 | -- 206 | -- Name: credentials_pkey; Type: CONSTRAINT; Schema: public; Owner: - 207 | -- 208 | 209 | ALTER TABLE ONLY credentials 210 | ADD CONSTRAINT credentials_pkey PRIMARY KEY (id); 211 | 212 | 213 | -- 214 | -- Name: goals_pkey; Type: CONSTRAINT; Schema: public; Owner: - 215 | -- 216 | 217 | ALTER TABLE ONLY goals 218 | ADD CONSTRAINT goals_pkey PRIMARY KEY (id); 219 | 220 | 221 | -- 222 | -- Name: scores_pkey; Type: CONSTRAINT; Schema: public; Owner: - 223 | -- 224 | 225 | ALTER TABLE ONLY scores 226 | ADD CONSTRAINT scores_pkey PRIMARY KEY (id); 227 | 228 | 229 | -- 230 | -- Name: fk__goals_provider_id; Type: INDEX; Schema: public; Owner: - 231 | -- 232 | 233 | CREATE INDEX fk__goals_provider_id ON goals USING btree (credential_id); 234 | 235 | 236 | -- 237 | -- Name: index_credentials_on_provider_name_and_beeminder_user_id; Type: INDEX; Schema: public; Owner: - 238 | -- 239 | 240 | CREATE UNIQUE INDEX index_credentials_on_provider_name_and_beeminder_user_id ON credentials USING btree (provider_name, beeminder_user_id); 241 | 242 | 243 | -- 244 | -- Name: index_credentials_on_provider_name_and_uid; Type: INDEX; Schema: public; Owner: - 245 | -- 246 | 247 | CREATE UNIQUE INDEX index_credentials_on_provider_name_and_uid ON credentials USING btree (provider_name, uid); 248 | 249 | 250 | -- 251 | -- Name: index_goals_on_metric_key; Type: INDEX; Schema: public; Owner: - 252 | -- 253 | 254 | CREATE INDEX index_goals_on_metric_key ON goals USING btree (metric_key); 255 | 256 | 257 | -- 258 | -- Name: index_goals_on_slug_and_credential_id; Type: INDEX; Schema: public; Owner: - 259 | -- 260 | 261 | CREATE UNIQUE INDEX index_goals_on_slug_and_credential_id ON goals USING btree (slug, credential_id); 262 | 263 | 264 | -- 265 | -- Name: index_scores_on_goal_id; Type: INDEX; Schema: public; Owner: - 266 | -- 267 | 268 | CREATE INDEX index_scores_on_goal_id ON scores USING btree (goal_id); 269 | 270 | 271 | -- 272 | -- Name: index_scores_on_goal_id_and_timestamp; Type: INDEX; Schema: public; Owner: - 273 | -- 274 | 275 | CREATE UNIQUE INDEX index_scores_on_goal_id_and_timestamp ON scores USING btree (goal_id, "timestamp"); 276 | 277 | 278 | -- 279 | -- Name: index_users_on_beeminder_user_id; Type: INDEX; Schema: public; Owner: - 280 | -- 281 | 282 | CREATE UNIQUE INDEX index_users_on_beeminder_user_id ON users USING btree (beeminder_user_id); 283 | 284 | 285 | -- 286 | -- Name: unique_schema_migrations; Type: INDEX; Schema: public; Owner: - 287 | -- 288 | 289 | CREATE UNIQUE INDEX unique_schema_migrations ON schema_migrations USING btree (version); 290 | 291 | 292 | -- 293 | -- Name: fk_credentials_beeminder_user_id; Type: FK CONSTRAINT; Schema: public; Owner: - 294 | -- 295 | 296 | ALTER TABLE ONLY credentials 297 | ADD CONSTRAINT fk_credentials_beeminder_user_id FOREIGN KEY (beeminder_user_id) REFERENCES users(beeminder_user_id); 298 | 299 | 300 | -- 301 | -- Name: fk_goals_provider_id; Type: FK CONSTRAINT; Schema: public; Owner: - 302 | -- 303 | 304 | ALTER TABLE ONLY goals 305 | ADD CONSTRAINT fk_goals_provider_id FOREIGN KEY (credential_id) REFERENCES credentials(id); 306 | 307 | 308 | -- 309 | -- Name: fk_scores_goal_id; Type: FK CONSTRAINT; Schema: public; Owner: - 310 | -- 311 | 312 | ALTER TABLE ONLY scores 313 | ADD CONSTRAINT fk_scores_goal_id FOREIGN KEY (goal_id) REFERENCES goals(id); 314 | 315 | 316 | -- 317 | -- PostgreSQL database dump complete 318 | -- 319 | 320 | SET search_path TO "$user", public; 321 | 322 | INSERT INTO schema_migrations (version) VALUES 323 | ('20140714204840'), 324 | ('20140729165051'), 325 | ('20150719185636'), 326 | ('20150721223451'), 327 | ('20150907205208'), 328 | ('20150908234553'), 329 | ('20150909225051'), 330 | ('20150926194732'), 331 | ('20150926210204'), 332 | ('20160316220158'), 333 | ('20160519212602'), 334 | ('20170125204752'); 335 | 336 | 337 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/templates/haml/scaffold/_form.html.haml: -------------------------------------------------------------------------------- 1 | = simple_form_for(@<%= singular_table_name %>) do |f| 2 | = f.error_notification 3 | 4 | .form-inputs 5 | <%- attributes.each do |attribute| -%> 6 | = f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> 7 | <%- end -%> 8 | 9 | .form-actions 10 | = f.button :submit 11 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

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

63 |
64 |

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

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

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

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

We're sorry, but something went wrong.

62 |
63 |

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

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/public/favicon.ico -------------------------------------------------------------------------------- /public/humans.txt: -------------------------------------------------------------------------------- 1 | /* the humans responsible & colophon */ 2 | /* humanstxt.org */ 3 | 4 | 5 | /* TEAM */ 6 | : 7 | Site: 8 | Twitter: 9 | Location: 10 | 11 | /* THANKS */ 12 | Daniel Kehoe (@rails_apps) for the RailsApps project 13 | 14 | /* SITE */ 15 | Standards: HTML5, CSS3 16 | Components: jQuery 17 | Software: Ruby on Rails 18 | 19 | /* GENERATED BY */ 20 | Rails Composer: http://railscomposer.com/ 21 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /spec/adapters/googlefit_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe GooglefitAdapter do 4 | let(:subject) { GooglefitAdapter } 5 | let(:bad_credentials) { BaseAdapter::InvalidCredentials } 6 | let(:bad_auth) { BaseAdapter::AuthorizationError } 7 | 8 | describe "validations" do 9 | context "when credentials missing token" do 10 | let(:credentials) { {} } 11 | 12 | it "is invalid" do 13 | expect(subject.valid_credentials?(credentials)).to be false 14 | end 15 | 16 | it "Raise error on instantiation" do 17 | expect { subject.new(credentials) }.to raise_error(bad_credentials) 18 | end 19 | end 20 | 21 | context "When credentials have token" do 22 | let(:credentials) { { token: "sometoken" } } 23 | 24 | it "is valid" do 25 | expect(subject.valid_credentials?(credentials)).to be true 26 | end 27 | 28 | it "Does not raise error on instantiation" do 29 | expect { subject.new(credentials) }.not_to raise_error 30 | end 31 | end 32 | end 33 | describe "When there is problem with authorization" do 34 | it "returns our internal authorization error" do 35 | adapter = subject.new(token: "bad token") 36 | google_bad_auth = Signet::AuthorizationError.new({}) 37 | allow(adapter).to receive(:authorization).with(no_args) 38 | .and_raise(google_bad_auth) 39 | expect do 40 | adapter.fetch_steps 41 | end.to raise_error do |error| 42 | expect(error).to be_kind_of(bad_auth) 43 | expect(error.cause).to eq(google_bad_auth) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/adapters/pocket_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require "active_support/all" 2 | describe PocketAdapter do 3 | let(:subject) { PocketAdapter } 4 | 5 | describe "validations" do 6 | context "when credentials missing token" do 7 | let(:credentials) { {} } 8 | 9 | it "is invalid" do 10 | expect(subject.valid_credentials?(credentials)).to be false 11 | end 12 | 13 | it "Raise error on instantiation" do 14 | error_klass = BaseAdapter::InvalidCredentials 15 | expect { subject.new(credentials) }.to raise_error(error_klass) 16 | end 17 | end 18 | 19 | context "When credentials have token" do 20 | let(:credentials) { { token: "sometoken" } } 21 | 22 | it "is valid" do 23 | expect(subject.valid_credentials?(credentials)).to be true 24 | end 25 | 26 | it "Does not raise error on instantiation" do 27 | expect { subject.new(credentials) }.not_to raise_error 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/controllers/sessions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require "active_support/all" 2 | require "rails_helper" 3 | describe SessionsController, :omniauth do 4 | before do 5 | request.env["omniauth.auth"] = mock_auth 6 | end 7 | 8 | describe "#create" do 9 | it "creates a user" do 10 | expect do 11 | post :create, params: { provider: :twitter } 12 | end.to change { User.count }.by(1) 13 | end 14 | 15 | it "creates a session" do 16 | expect(session[:beeminder_user_id]).to be_nil 17 | post :create, params: { provider: :twitter } 18 | expect(session[:beeminder_user_id]).not_to be_nil 19 | end 20 | 21 | it "redirects to the home page" do 22 | post :create, params: { provider: :twitter } 23 | expect(response).to redirect_to root_url 24 | end 25 | end 26 | 27 | describe "#destroy" do 28 | before do 29 | post :create, params: { provider: :twitter } 30 | end 31 | 32 | it "resets the session" do 33 | expect(session[:beeminder_user_id]).not_to be_nil 34 | delete :destroy 35 | expect(session[:beeminder_user_id]).to be_nil 36 | end 37 | 38 | it "redirects to the home page" do 39 | delete :destroy 40 | expect(response).to redirect_to root_url 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/factories/credential_factory.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: credentials 4 | # 5 | # id :integer not null, primary key 6 | # beeminder_user_id :string not null 7 | # provider_name :string not null 8 | # uid :string default(""), not null 9 | # info :json not null 10 | # credentials :json not null 11 | # extra :json not null 12 | # created_at :datetime 13 | # updated_at :datetime 14 | # password :string default(""), not null 15 | # 16 | 17 | FactoryGirl.define do 18 | factory :credential do 19 | user 20 | provider_name :pocket 21 | uid { |i| "uid_#{i}" } 22 | credentials(token: "token") 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/factories/goal_factory.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: goals 4 | # 5 | # id :integer not null, primary key 6 | # credential_id :integer not null 7 | # slug :string not null 8 | # last_value :float 9 | # params :json not null 10 | # metric_key :string not null 11 | # active :boolean default(TRUE), not null 12 | # fail_count :integer default(0), not null 13 | # 14 | 15 | FactoryGirl.define do 16 | factory :goal do 17 | slug { |i| "slug_#{i}" } 18 | metric_key :article_days_linear 19 | credential 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/factories/user_factory.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # beeminder_user_id :string not null, primary key 6 | # created_at :datetime 7 | # updated_at :datetime 8 | # 9 | 10 | FactoryGirl.define do 11 | factory :user do 12 | beeminder_user_id { SecureRandom.hex(8) } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/features/googlefit_goals_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe "Googlefit goals" do 4 | scenario "create hourly steps goal" do 5 | user = create(:user) 6 | mock_current_user user 7 | mock_beeminder_goals(user, %w(slug1 slug2 slug3)) 8 | mock_auth :googlefit 9 | mock_provider_score 10 | visit root_path 11 | 12 | expect(user.credentials).to be_empty 13 | expect(user.goals).to be_empty 14 | 15 | page.click_link("Hourly Steps") 16 | expect(user.credentials.count).to eq(1) 17 | expect(user.credentials.first.provider_name).to eq("googlefit") 18 | expect(page.current_path).to eq(root_path) 19 | 20 | page.click_link("Hourly Steps") 21 | expect(page).to have_content "Google Fit Hourly Steps" 22 | expect(page).to have_content "Goal Configuration" 23 | 24 | page.select "slug2", from: "goal_slug" 25 | page.click_button "Save" 26 | expect(page).to have_content("Updated successfully!") 27 | expect(user.goals.count).to eq(1) 28 | goal = user.goals.first 29 | expect(goal.metric_key).to eq("hourly_steps") 30 | expect(goal.slug).to eq("slug2") 31 | expect(page).to have_css("#configured-goals", text: "Google Fit - Hourly Steps") 32 | 33 | page.click_link("Hourly Steps") 34 | expect(page).to have_select("goal_slug", selected: "slug2") 35 | click_link "Delete" 36 | expect(page).to have_content("Deleted successfully!") 37 | expect(user.reload.goals).to be_empty 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/features/home_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe "Home page" do 4 | scenario "anonymous user logs in to the app" do 5 | visit root_path 6 | expect(page).to have_content("Welcome") 7 | expect(page).to have_content("sign in") 8 | end 9 | 10 | scenario "User signs in " do 11 | visit root_path 12 | mock_auth 13 | click_link "Sign in" 14 | expect(page.current_path).to eq(root_path) 15 | expect(@page).not_to have_content("Please sign in") 16 | expect(@page).not_to have_content("Welcome") 17 | end 18 | context "signed in user" do 19 | scenario "without goals" do 20 | user = create(:user) 21 | mock_current_user user 22 | visit root_path 23 | expect(page).not_to have_content("Reload") 24 | expect(page).to have_content("Welcome") 25 | end 26 | scenario "that has goals" do 27 | goal = create :goal 28 | mock_current_user goal.user 29 | visit root_path 30 | expect(page).to have_content("Reload") 31 | fake_worker = double(BeeminderWorker) 32 | expect(BeeminderWorker).to receive(:new).and_return(fake_worker) 33 | expect(fake_worker).to( 34 | receive(:perform).with(beeminder_user_id: goal.user.beeminder_user_id) 35 | ) 36 | click_link "Reload" 37 | expect(page).to have_content("Scores updated") 38 | expect(page.current_path).to eq(root_path) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/features/providers/main_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | feature "Home page" do 3 | context "anonymouse user" do 4 | scenario "redirected to root" do 5 | visit root_path 6 | expect(page.current_path).to eq(root_path) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/features/trello_goals_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe "Trello goals" do 4 | scenario "create cards backlog goal" do 5 | user = create(:user) 6 | mock_current_user user 7 | mock_beeminder_goals(user, %w(slug1 slug2 slug3)) 8 | mock_auth :trello 9 | mock_provider_score 10 | visit root_path 11 | 12 | expect(user.credentials).to be_empty 13 | expect(user.goals).to be_empty 14 | 15 | page.click_link("Cards backlog") 16 | expect(user.credentials.count).to eq(1) 17 | expect(user.credentials.first.provider_name).to eq("trello") 18 | expect(page.current_path).to eq(root_path) 19 | 20 | mock_trello_boards 21 | page.click_link("Cards backlog") 22 | expect(page).to have_content "Trello Cards backlog" 23 | expect(page).to have_content "Goal Configuration" 24 | 25 | page.select "slug2", from: "goal_slug" 26 | page.select "List2", from: "goal_params_list_ids" 27 | page.select "List3", from: "goal_params_list_ids" 28 | page.click_button "Save" 29 | expect(page).to have_content("Updated successfully!") 30 | goal = user.goals.first 31 | expect(goal.metric_key).to eq("idle_days_linear") 32 | expect(goal.slug).to eq("slug2") 33 | expect(page).to have_css("#configured-goals", text: "Trello - Cards backlog") 34 | 35 | page.click_link("Cards backlog") 36 | expect(page).to have_select("goal_slug", selected: "slug2") 37 | expect(page).to have_select "goal_params_list_ids", 38 | selected: %w(List2 List3) 39 | 40 | click_link "Delete" 41 | expect(page).to have_content("Deleted successfully!") 42 | expect(user.reload.goals).to be_empty 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/features/users/session_spec.rb: -------------------------------------------------------------------------------- 1 | feature "Sign in", :omniauth do 2 | context "with valid account" do 3 | scenario "user can sign in with valid account" do 4 | visit root_path 5 | mock_auth 6 | click_link "sign in" 7 | expect(page).to have_content("Sign out") 8 | 9 | click_link "Sign out" 10 | expect(page).to have_content "Signed out" 11 | expect(page.current_path).to eq(root_path) 12 | expect(page).to have_content("Sign in") 13 | end 14 | end 15 | 16 | scenario "user cannot sign in with invalid account" do 17 | OmniAuth.config.mock_auth[:beeminder] = :invalid_credentials 18 | visit root_path 19 | expect(page).to have_content("Sign in") 20 | click_link "Sign in" 21 | expect(page).to have_content("Authentication error") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/metrics/beeminder/compose_goals_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe "Compose goals metric" do 4 | let(:subject) { PROVIDERS[:beeminder].find_metric(:compose_goals) } 5 | 6 | context "params validations" do 7 | it "requires slug_sources key" do 8 | errors = subject.param_errors({}) 9 | expect(errors.first).to match(/source_slugs hash/i) 10 | end 11 | context "when slug_sources exists" do 12 | it "accepts empty hash" do 13 | params = { "source_slugs" => { 14 | 15 | } } 16 | errors = subject.param_errors(params) 17 | expect(errors).to be_empty 18 | end 19 | it "rejects long slugs" do 20 | params = { 21 | "source_slugs" => { 22 | "a" * 251 => "1", 23 | }, 24 | } 25 | errors = subject.param_errors(params) 26 | expect(errors.first).to match(/Slug too long/i) 27 | end 28 | it "rejects non numeric factors" do 29 | params = { 30 | "source_slugs" => { 31 | "aasdf" => "nan", 32 | }, 33 | } 34 | errors = subject.param_errors(params) 35 | expect(errors.first).to match(/must be numbers/i) 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/metrics/googlefit/bed_time_lag_minutes_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe "Bed time lag" do 4 | let(:subject) { PROVIDERS[:googlefit].find_metric(:bed_time_lag_minutes) } 5 | 6 | start_ts = Time.zone.parse("2015-09-10") 7 | def make_point(timestamp, value = 1) 8 | # tz = ActiveSupport::TimeZone.new("UTC") 9 | double start_time_nanos: (timestamp.to_i * 1_000_000_000), 10 | value: [double(int_val: value)] 11 | end 12 | 13 | let(:tz) { ActiveSupport::TimeZone.new(params["timezone"]) } 14 | describe "calculating lag" do 15 | context "when there is not timezone" do 16 | it "returns empty results" do 17 | adapter = double fetch_sleeps: [make_point(start_ts, 1)] 18 | result = subject.call(adapter, {}) 19 | expect(result.value).to eq("Please configure timezone and reload page") 20 | 21 | end 22 | end 23 | context "where there were sleeps" do 24 | context "two sleeps in one day" do 25 | let(:params) do 26 | { 27 | "timezone": "Dublin", 28 | "bed_time_hour": 22, 29 | "bed_time_minute": 0, 30 | }.with_indifferent_access 31 | end 32 | it "returns two results" do 33 | points = [ 34 | make_point((start_ts + 23.hours)), 35 | make_point((start_ts + 21.hours)), 36 | ] 37 | 38 | adapter = double fetch_sleeps: points 39 | results = subject.call(adapter, params) 40 | expect(results.count).to eq(1) 41 | first_dp = Datapoint.new(timestamp: tz.at(start_ts).beginning_of_day, 42 | value: 0, 43 | unique: true) 44 | expect(results[0]).to eq(first_dp) 45 | end 46 | end 47 | context "two sleeps in two days" do 48 | let(:params) do 49 | { 50 | "timezone": "UTC", 51 | "bed_time_hour": 22, 52 | "bed_time_minute": 30, 53 | }.with_indifferent_access 54 | end 55 | it "returns two results" do 56 | points = [ 57 | make_point((start_ts + 22.hours + 29.minutes)), 58 | make_point((start_ts + 1.day + 22.hours + 31.minutes)), 59 | ] 60 | 61 | adapter = double fetch_sleeps: points 62 | results = subject.call(adapter, params) 63 | expect(results.count).to eq(2) 64 | first_dp = Datapoint.new(timestamp: start_ts, 65 | value: 0, 66 | unique: true) 67 | expect(results[0]).to eq(first_dp) 68 | 69 | second_dp = Datapoint.new(timestamp: (start_ts + 1.day), 70 | value: 1, 71 | unique: true) 72 | expect(results[1]).to eq(second_dp) 73 | end 74 | end 75 | context "Sleep after midnight" do 76 | let(:params) do 77 | { 78 | "timezone": "UTC", 79 | "bed_time_hour": 23, 80 | "bed_time_minute": 59, 81 | }.with_indifferent_access 82 | end 83 | it "assigns it to the previous day" do 84 | points = [ 85 | make_point((start_ts + 1.day + 11.hours + 58.minutes)), 86 | ] 87 | 88 | adapter = double fetch_sleeps: points 89 | results = subject.call(adapter, params) 90 | expect(results.count).to eq(1) 91 | first_dp = Datapoint.new(timestamp: start_ts, 92 | value: 11 * 60 + 59, 93 | unique: true) 94 | expect(results[0]).to eq(first_dp) 95 | end 96 | end 97 | context "In a different timezone" do 98 | let(:params) do 99 | { 100 | "timezone": "Hawaii", 101 | "bed_time_hour": 21, 102 | "bed_time_minute": 0, 103 | }.with_indifferent_access 104 | end 105 | it "assigns it to the previous day" do 106 | hawaii_ts = tz.parse("2015-09-27").beginning_of_day 107 | points = [ 108 | make_point(hawaii_ts + 21.hours + 18.minutes), 109 | ] 110 | 111 | adapter = double fetch_sleeps: points 112 | Timecop.freeze(start_ts) do 113 | results = subject.call(adapter, params) 114 | expect(results.count).to eq(1) 115 | first_dp = Datapoint.new(timestamp: hawaii_ts.beginning_of_day, 116 | value: 18, 117 | unique: true) 118 | expect(results[0]).to eq(first_dp) 119 | end 120 | end 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /spec/metrics/googlefit/hourly_steps_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe "Hourly step" do 4 | let(:subject) { PROVIDERS[:googlefit].find_metric(:hourly_steps) } 5 | 6 | def make_point(timestamp, value) 7 | double start_time_nanos: (timestamp.to_i * 1_000_000_000), 8 | value: [double(int_val: value)] 9 | end 10 | 11 | describe "calculating steps" do 12 | context "when there were no steps" do 13 | it "returns empty results" do 14 | adapter = double fetch_steps: [] 15 | expect(subject.call(adapter)).to be_empty 16 | end 17 | end 18 | context "when there were three walks in two hours" do 19 | it "returns two results" do 20 | start_ts = Time.zone.parse("2015-09-10") 21 | points = [ 22 | make_point((start_ts + 0.minutes), 100), 23 | make_point((start_ts + 59.minutes), 50), 24 | make_point((start_ts + 90.minutes), 200), 25 | ] 26 | 27 | adapter = double fetch_steps: points 28 | results = subject.call(adapter) 29 | expect(results.count).to eq(2) 30 | first_dp = Datapoint.new(timestamp: start_ts, 31 | value: 150, 32 | unique: true) 33 | expect(results[0]).to eq(first_dp) 34 | 35 | second_dp = Datapoint.new(timestamp: (start_ts + 60.minutes), 36 | value: 200, 37 | unique: true) 38 | expect(results[1]).to eq(second_dp) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/metrics/pocket/article_days_linear_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe "Days backlog" do 4 | let(:subject) { PROVIDERS[:pocket].find_metric(:article_days_linear) } 5 | 6 | context "when there are 5 articles from the last 5 days" do 7 | let(:start_ts) { Time.zone.parse("2015-01-01") } 8 | 9 | it "calculates 15" do 10 | Timecop.freeze(start_ts) do 11 | articles = (1..5).map do |i| 12 | { "time_added" => i.days.ago } 13 | end 14 | adapter = double articles: articles 15 | expect(subject.call(adapter)).to eq( 16 | Datapoint.new(timestamp: nil, value: 15) 17 | ) 18 | end 19 | end 20 | 21 | it "calculates 50 a week later" do 22 | Timecop.freeze(start_ts) do 23 | articles = (1..5).map do |i| 24 | { "time_added" => (7 + i).days.ago } 25 | end 26 | adapter = double articles: articles 27 | expect(subject.call(adapter)).to eq( 28 | Datapoint.new(timestamp: nil, value: 50) 29 | ) 30 | end 31 | end 32 | 33 | it "rounds down to avoid fractions" do 34 | Timecop.freeze(start_ts) do 35 | adapter = double articles: [{ "time_added" => 47.hours.ago }] 36 | expect(subject.call(adapter)).to eq( 37 | Datapoint.new(timestamp: nil, value: 1) 38 | ) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/metrics/trello/idle_days_linear_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe "Days backlog" do 4 | let(:subject) { PROVIDERS[:trello].find_metric(:idle_days_linear) } 5 | let(:list_ids) { %w(list1 list2) } 6 | let(:options) { { list_ids: list_ids } } 7 | 8 | context "when there are 5 cards from the last 5 days" do 9 | let(:start_ts) { Time.zone.parse("2015-01-01") } 10 | 11 | it "calculates 15" do 12 | Timecop.freeze(start_ts) do 13 | cards = (1..5).map do |i| 14 | double last_activity_date: i.days.ago 15 | end 16 | expect(adapter = double).to receive(:cards) 17 | .with(list_ids).and_return(cards) 18 | 19 | expect(subject.call(adapter, options)).to eq( 20 | Datapoint.new(timestamp: nil, value: 15) 21 | ) 22 | end 23 | end 24 | 25 | it "calculates 50 a week later" do 26 | Timecop.freeze(start_ts) do 27 | cards = (1..5).map do |i| 28 | double last_activity_date: (7 + i).days.ago 29 | end 30 | expect(adapter = double).to receive(:cards) 31 | .with(list_ids).and_return(cards) 32 | 33 | expect(subject.call(adapter, options)).to eq( 34 | Datapoint.new(timestamp: nil, value: 50) 35 | ) 36 | end 37 | end 38 | 39 | it "rounds down to avoid fractions" do 40 | Timecop.freeze(start_ts) do 41 | cards = [double(last_activity_date: 47.hours.ago)] 42 | expect(adapter = double).to receive(:cards) 43 | .with(list_ids).and_return(cards) 44 | expect(subject.call(adapter, options)).to eq( 45 | Datapoint.new(timestamp: nil, value: 1) 46 | ) 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/models/datapoint_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe Datapoint do 4 | ts = Time.zone.at(0) 5 | 6 | context "equality" do 7 | shared_examples "equal as can be" do |dp1, dp2| 8 | it "#==" do 9 | expect(dp1).to eq(dp2) 10 | end 11 | 12 | it "#.hash" do 13 | expect(dp1.hash).to eq(dp2.hash) 14 | end 15 | 16 | it "#.eql?" do 17 | expect(dp1.eql?(dp2)).to be true 18 | end 19 | 20 | it "can be subtracted from array" do 21 | expect([dp1] - [dp2]).to be_empty 22 | end 23 | end 24 | context "When value has different type" do 25 | it_behaves_like "equal as can be", 26 | Datapoint.new(value: 1, timestamp: ts), 27 | Datapoint.new(value: 1.0, timestamp: ts) 28 | end 29 | 30 | context "When id has different type" do 31 | it_behaves_like "equal as can be", 32 | Datapoint.new(id: "1", value: 1, timestamp: ts), 33 | Datapoint.new(id: 1, value: 1, timestamp: ts) 34 | end 35 | 36 | context "Value accuracy" do 37 | # This test verify floating point accuracy subtlties does not cause inequality 38 | it_behaves_like "equal as can be", 39 | Datapoint.new(value: (0.1 + 0.2), timestamp: ts), 40 | Datapoint.new(value: 0.3, timestamp: ts) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # beeminder_user_id :string not null, primary key 6 | # created_at :datetime 7 | # updated_at :datetime 8 | # 9 | 10 | describe User do 11 | let(:user) { build(:user) } 12 | 13 | describe "Validations" do 14 | it "factory creates valid model" do 15 | expect(user).to be_valid 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require "spec_helper" 3 | require File.expand_path("../../config/environment", __FILE__) 4 | require "rspec/rails" 5 | 6 | Dir[Rails.root.join("spec/support/*.rb")].each { |f| require f } 7 | 8 | # ActiveRecord::Migration.maintain_test_schema! 9 | 10 | OmniAuth.config.test_mode = true 11 | RSpec.configure do |config| 12 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 13 | config.use_transactional_fixtures = true 14 | config.infer_spec_type_from_file_location! 15 | config.filter_gems_from_backtrace( 16 | "actionpack", "actionview", "activerecord", "activesupport", "capybara", "omniauth", "rack", "railties", "request_store", "rollbar" 17 | ) 18 | config.include OmniauthTestHelpers 19 | config.include ThirdPartyMocks 20 | end 21 | -------------------------------------------------------------------------------- /spec/routing/goals_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe "Goal routes" do 4 | %w( 5 | /goals/googlefit/hourly_steps 6 | /goals/pocket/article_days_linear 7 | /goals/trello/idle_days_linear 8 | ).each do |known_path| 9 | it "has route to create #{known_path}" do 10 | expect(get: known_path).to be_routable 11 | end 12 | end 13 | 14 | %w( 15 | /goals/asdf/unknown 16 | /goals/googlefit/asdf 17 | /goals/asdff 18 | ).each do |unknown_path| 19 | it "doesn't have route for unknown path #{unknown_path}" do 20 | expect(get: unknown_path).not_to be_routable 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/services/identity_resolver_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe IdentityResolver do 4 | let(:resolver) { IdentityResolver.new current_user, auth } 5 | let(:token) { "nekot" } 6 | 7 | def resolve(current_user, auth) 8 | IdentityResolver.new(current_user, auth).credential 9 | end 10 | 11 | describe "when credentials exist" do 12 | let(:c) { create(:credential) } 13 | let(:auth) do 14 | { "uid" => c.uid, 15 | "provider" => c.provider_name } 16 | end 17 | 18 | context "when no user is logged in" do 19 | it "finds it" do 20 | session_user = nil 21 | expect(resolve(session_user, auth)).to eq(c) 22 | end 23 | end 24 | 25 | context "when the owner is logged in" do 26 | let(:session_user) { c.user } 27 | it "finds it" do 28 | session_user = c.user 29 | expect(resolve(session_user, auth)).to eq(c) 30 | end 31 | end 32 | 33 | context "when another user is logged in" do 34 | it "finds it" do 35 | session_user = create :user 36 | expect(resolve(session_user, auth)).to eq(c) 37 | end 38 | end 39 | end 40 | 41 | describe "when credentials are new" do 42 | context "and no user is logged in" do 43 | let(:session_user) { nil } 44 | context "when provider is beeminder" do 45 | let(:auth) do 46 | { "provider" => "beeminder", 47 | "uid" => "fictitious_user" } 48 | end 49 | it "creates a new credential" do 50 | c = nil 51 | expect do 52 | c = resolve(session_user, auth) 53 | end.to change(Credential, :count).by(1) 54 | expect(c).to be_persisted 55 | expect(c.provider_name).to eq(auth["provider"]) 56 | expect(c.uid).to eq(auth["uid"]) 57 | expect(c.user.beeminder_user_id).to eq(auth["uid"]) 58 | end 59 | end 60 | 61 | context "when provider is not beeminder" do 62 | let(:auth) do 63 | { "provider" => "pocket", 64 | "uid" => "fictitious_user" } 65 | end 66 | it "does not create a new credential" do 67 | c = 1 68 | expect do 69 | c = resolve(session_user, auth) 70 | end.not_to change(Credential, :count) 71 | expect(c).to be_nil 72 | end 73 | end 74 | end 75 | context "when a user is logged in" do 76 | let(:session_user) { create(:user) } 77 | let(:auth) do 78 | { "provider" => "pocket", 79 | "uid" => "fictitious_user" } 80 | end 81 | it "creates him a new credential" do 82 | c = nil 83 | expect do 84 | c = resolve(session_user, auth) 85 | end.to change(Credential, :count).by(1) 86 | expect(c).to be_persisted 87 | expect(c.provider_name).to eq(auth["provider"]) 88 | expect(c.uid).to eq(auth["uid"]) 89 | expect(c.user).to eq(session_user) 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "simplecov" 2 | SimpleCov.start 3 | 4 | RSpec.configure do |_config| 5 | # The settings below are suggested to provide a good initial experience 6 | # with RSpec, but feel free to customize to your heart's content. 7 | # # These two settings work together to allow you to limit a spec run 8 | # # to individual examples or groups you care about by tagging them with 9 | # # `:focus` metadata. When nothing is tagged with `:focus`, all examples 10 | # # get run. 11 | # config.filter_run :focus 12 | # config.run_all_when_everything_filtered = true 13 | # 14 | # # Many RSpec users commonly either run the entire suite or an individual 15 | # # file, and it's useful to allow more verbose output when running an 16 | # # individual spec file. 17 | # if config.files_to_run.one? 18 | # # Use the documentation formatter for detailed output, 19 | # # unless a formatter has already been configured 20 | # # (e.g. via a command-line flag). 21 | # config.default_formatter = 'doc' 22 | # end 23 | # 24 | # # Print the 10 slowest examples and example groups at the 25 | # # end of the spec run, to help surface which specs are running 26 | # # particularly slow. 27 | # config.profile_examples = 10 28 | # 29 | # # Run specs in random order to surface order dependencies. If you find an 30 | # # order dependency and want to debug it, you can fix the order by providing 31 | # # the seed, which is printed after each run. 32 | # # --seed 1234 33 | # config.order = :random 34 | # 35 | # # Seed global randomization in this process using the `--seed` CLI option. 36 | # # Setting this allows you to use `--seed` to deterministically reproduce 37 | # # test failures related to randomization by passing the same `--seed` value 38 | # # as the one that triggered the failure. 39 | # Kernel.srand config.seed 40 | # 41 | # # rspec-expectations config goes here. You can use an alternate 42 | # # assertion/expectation library such as wrong or the stdlib/minitest 43 | # # assertions if you prefer. 44 | # config.expect_with :rspec do |expectations| 45 | # # Enable only the newer, non-monkey-patching expect syntax. 46 | # # For more details, see: 47 | # # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 48 | # expectations.syntax = :expect 49 | # end 50 | # 51 | # # rspec-mocks config goes here. You can use an alternate test double 52 | # # library (such as bogus or mocha) by changing the `mock_with` option here. 53 | # config.mock_with :rspec do |mocks| 54 | # # Enable only the newer, non-monkey-patching expect syntax. 55 | # # For more details, see: 56 | # # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 57 | # mocks.syntax = :expect 58 | # 59 | # # Prevents you from mocking or stubbing a method that does not exist on 60 | # # a real object. This is generally recommended. 61 | # mocks.verify_partial_doubles = true 62 | # end 63 | end 64 | -------------------------------------------------------------------------------- /spec/support/capybara.rb: -------------------------------------------------------------------------------- 1 | Capybara.asset_host = "http://localhost:3000" 2 | -------------------------------------------------------------------------------- /spec/support/factory_girl.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include FactoryGirl::Syntax::Methods 3 | end 4 | -------------------------------------------------------------------------------- /spec/support/omniauth_test_helpers.rb: -------------------------------------------------------------------------------- 1 | module OmniauthTestHelpers 2 | FAKE_AUTH_ATTR = { 3 | "uid" => "mock_uid", 4 | "user_info" => { 5 | "name" => "mockuser", 6 | "image" => "mock_user_thumbnail_url", 7 | }, 8 | "credentials" => { 9 | "token" => "mock_token", 10 | "secret" => "mock_secret", 11 | }, 12 | }.freeze 13 | def mock_auth(provider = :beeminder) 14 | OmniAuth.config.mock_auth = {} 15 | OmniAuth.config.mock_auth[provider] = FAKE_AUTH_ATTR.merge( 16 | "provider" => provider.to_s 17 | ) 18 | end 19 | 20 | def mock_current_user(user = build_stubbed(:user)) 21 | allow_any_instance_of(ApplicationController).to( 22 | receive(:current_user).and_return(user) 23 | ) 24 | user 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/support/third_party_mocks.rb: -------------------------------------------------------------------------------- 1 | module ThirdPartyMocks 2 | def generate_fake_goals(slug_names) 3 | slug_names.map do |name| 4 | double slug: name 5 | end 6 | end 7 | 8 | def mock_beeminder_goals(user, slug_name) 9 | fake_goals = generate_fake_goals(slug_name) 10 | fake_client = double(goals: fake_goals) 11 | allow(user).to receive(:client).and_return(fake_client) 12 | end 13 | 14 | def mock_trello_boards 15 | fake_lists = (1..4).map { |i| ["List#{i}", i] } 16 | allow_any_instance_of(TrelloAdapter).to receive(:list_options).and_return(fake_lists) 17 | end 18 | 19 | def mock_provider_score 20 | fake_scores = [ 21 | Datapoint.new(timestamp: Time.zone.at(1_437_775_585), value: 12_423), 22 | ] 23 | allow_any_instance_of(Metric).to( 24 | receive(:call).and_return(fake_scores) 25 | ) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/support/vcr_helper.rb: -------------------------------------------------------------------------------- 1 | VCR.configure do |c| 2 | c.cassette_library_dir = "support/vcr_cassettes" 3 | c.hook_into :webmock 4 | c.ignore_hosts "codeclimate.com" 5 | end 6 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsubery/quantifier/3512f020e23df3d20ea3a044af6f47ae86137b68/vendor/assets/stylesheets/.keep --------------------------------------------------------------------------------