From 648632205dbd6f92ce8328bfff883573f4bf4973 Mon Sep 17 00:00:00 2001 From: Dan Boger Date: Sat, 7 Dec 2013 20:00:03 -0800 Subject: [PATCH 0001/1107] Add 'f' shortcut to go to feeds page. --- app/views/partials/_action_bar.erb | 5 +++++ app/views/partials/_shortcuts.erb | 1 + config/locales/en.yml | 1 + 3 files changed, 7 insertions(+) diff --git a/app/views/partials/_action_bar.erb b/app/views/partials/_action_bar.erb index 56cd25340..523e6e2da 100644 --- a/app/views/partials/_action_bar.erb +++ b/app/views/partials/_action_bar.erb @@ -39,6 +39,11 @@ if (refresh) refresh.click(); }); + Mousetrap.bind("f", function() { + var all_feeds = $("a#feeds")[0]; + if (all_feeds) all_feeds.click(); + }); + Mousetrap.bind("a", function() { var add_feed = $("a#add-feed")[0]; if (add_feed) add_feed.click(); diff --git a/app/views/partials/_shortcuts.erb b/app/views/partials/_shortcuts.erb index e19ee1134..e149a32b3 100644 --- a/app/views/partials/_shortcuts.erb +++ b/app/views/partials/_shortcuts.erb @@ -14,6 +14,7 @@
  • a: <%= t('partials.shortcuts.keys.a') %>
  • shift+a: <%= t('partials.shortcuts.keys.shifta') %>
  • +
  • f: <%= t('partials.shortcuts.keys.f') %>
  • r: <%= t('partials.shortcuts.keys.r') %>
  • : <%= t('partials.shortcuts.keys.left') %>
  • diff --git a/config/locales/en.yml b/config/locales/en.yml index d66428857..299df2eae 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -89,6 +89,7 @@ en: shortcuts: keys: a: Add a feed + f: Go to feeds page jk: Next/previous story left: Previous page m: Mark item as read/unread From 1ff9a7f19bc51b840ed42d8a6b78959ec9000cee Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Sun, 16 Feb 2014 12:59:52 +0100 Subject: [PATCH 0002/1107] Remove trailing whitespace --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 71d658858..a62615a74 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ### A [work-in-progress] self-hosted, anti-social RSS reader. -Stringer has no external dependencies, no social recommendations/sharing, and no fancy machine learning algorithms. +Stringer has no external dependencies, no social recommendations/sharing, and no fancy machine learning algorithms. But it does have keyboard shortcuts and was made with love! @@ -110,7 +110,7 @@ If you would like to translate Stringer to your preferred language, please use [ Clean up old read stories -If you are on the Heroku free plan, there is a 10k row limit so you will +If you are on the Heroku free plan, there is a 10k row limit so you will eventually run out of space. You can clean up old stories by running: @@ -123,7 +123,7 @@ task. # Development -Run the Ruby tests with `rspec`. +Run the Ruby tests with `rspec`. Run the Javascript tests with `rake test_js` and then open a browser to `http://localhost:4567/test`. From f074ce51263187458702c8bd51465cfef261c2de Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Sun, 16 Feb 2014 13:03:38 +0100 Subject: [PATCH 0003/1107] Update headers * Make the headers (mostly) hierarchical. * Replace horizontal lines with headers in the Niceties section. --- README.md | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index a62615a74..6569c4c80 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -##Stringer +# Stringer [![Build Status](https://travis-ci.org/swanson/stringer.png)](https://travis-ci.org/swanson/stringer) [![Code Climate](https://codeclimate.com/github/swanson/stringer.png)](https://codeclimate.com/github/swanson/stringer) [![Coverage Status](https://coveralls.io/repos/swanson/stringer/badge.png?branch=master)](https://coveralls.io/r/swanson/stringer) @@ -15,7 +15,7 @@ When `BIG_FREE_READER` shuts down, your instance of Stringer will still be kicki ![](screenshots/stories.png) ![](screenshots/feed.png) -# Installation +## Installation Stringer is a Ruby (2.0.0+) app based on Sinatra, ActiveRecord, PostgreSQL, Backbone.js and DelayedJob. @@ -56,21 +56,23 @@ heroku run rake db:migrate heroku restart ``` -# Niceties +## Niceties -Keyboard Shortcuts +### Keyboard Shortcuts You can access the keyboard shortcuts when using the app by hitting `?`. ![](screenshots/keyboard_shortcuts.png) ---- +### Using you own domain with Heroku You can run Stringer at `http://reader.yourdomain.com` using a CNAME. If you are on Heroku: -`heroku domains:add reader.yourdomain.com` +``` +heroku domains:add reader.yourdomain.com +``` Go to your registrar and add a CNAME: ``` @@ -81,7 +83,7 @@ Target: your-heroku-instance.herokuapp.com Wait a few minutes for changes to propagate. ---- +### Fever API Stringer implements a clone of [Fever's API](http://www.feedafever.com/api) so it can be used with any mobile client that supports Fever. @@ -98,7 +100,7 @@ Password: {your-stringer-password} If you have previously setup Stringer, you will need to migrate your database and run `rake change_password` for the API key to be setup properly. ---- +### Translations Stringer has been translated to [several other languages](config/locales). Your language can be set with the `LOCALE` environment variable. @@ -106,9 +108,7 @@ To set your locale on Heroku, run `heroku config:set LOCALE=en`. If you would like to translate Stringer to your preferred language, please use [LocaleApp](http://www.localeapp.com/projects/4637). ---- - -Clean up old read stories +### Clean up old read stories If you are on the Heroku free plan, there is a 10k row limit so you will eventually run out of space. @@ -121,13 +121,13 @@ By default, this removes read stories that are more than 30 days old (that are not starred). You can either run this manually or add it as a scheduled task. -# Development +## Development Run the Ruby tests with `rspec`. Run the Javascript tests with `rake test_js` and then open a browser to `http://localhost:4567/test`. -## Getting Started +### Getting Started To get started using Stringer for development simply run the following: @@ -141,11 +141,10 @@ The application will be running on port `5000` You can launch an interactive console (ala `rails c`) using `racksh` -# Acknowledgements +## Acknowledgements Most of the heavy-lifting is done by [`feedzirra`](https://github.com/pauldix/feedzirra) and [`feedbag`](https://github.com/dwillis/feedbag). General sexiness courtesy of [`Twitter Bootstrap`](http://twitter.github.io/bootstrap/) and [`Flat UI`](http://designmodo.github.io/Flat-UI/). -# Contact +## Contact Matt Swanson, [mdswanson.com](http://mdswanson.com) [@_swanson](http://twitter.com/_swanson) - From 3d8f0ff62174d2bbb2f49fd5ae75642b0cba9236 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Sun, 16 Feb 2014 13:10:10 +0100 Subject: [PATCH 0004/1107] Remove reference to BIG_FREE_READER shutdown It's been down for quite some time now. --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 6569c4c80..3ebe9bf84 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,6 @@ Stringer has no external dependencies, no social recommendations/sharing, and no But it does have keyboard shortcuts and was made with love! -When `BIG_FREE_READER` shuts down, your instance of Stringer will still be kicking. - ![](screenshots/instructions.png) ![](screenshots/stories.png) ![](screenshots/feed.png) From 846b85506a38f015281b29028f806e1db26804a4 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Sun, 16 Feb 2014 13:12:09 +0100 Subject: [PATCH 0005/1107] Adjust whitespace, add punctuation --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3ebe9bf84..d51261743 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Stringer + [![Build Status](https://travis-ci.org/swanson/stringer.png)](https://travis-ci.org/swanson/stringer) [![Code Climate](https://codeclimate.com/github/swanson/stringer.png)](https://codeclimate.com/github/swanson/stringer) [![Coverage Status](https://coveralls.io/repos/swanson/stringer/badge.png?branch=master)](https://coveralls.io/r/swanson/stringer) @@ -41,7 +42,7 @@ Load the app and follow the instructions to import your feeds and start using th --- -In the event that you need to change your password, run `heroku run rake change_password` from the app folder. +In the event that you need to change your password, run `heroku run rake change_password` from the app folder. ## Updating the app @@ -111,9 +112,7 @@ If you would like to translate Stringer to your preferred language, please use [ If you are on the Heroku free plan, there is a 10k row limit so you will eventually run out of space. -You can clean up old stories by running: - -`rake cleanup_old_stories` +You can clean up old stories by running: `rake cleanup_old_stories` By default, this removes read stories that are more than 30 days old (that are not starred). You can either run this manually or add it as a scheduled @@ -135,14 +134,16 @@ rake db:migrate foreman start ``` -The application will be running on port `5000` +The application will be running on port `5000`. -You can launch an interactive console (ala `rails c`) using `racksh` +You can launch an interactive console (ala `rails c`) using `racksh`. ## Acknowledgements + Most of the heavy-lifting is done by [`feedzirra`](https://github.com/pauldix/feedzirra) and [`feedbag`](https://github.com/dwillis/feedbag). General sexiness courtesy of [`Twitter Bootstrap`](http://twitter.github.io/bootstrap/) and [`Flat UI`](http://designmodo.github.io/Flat-UI/). ## Contact + Matt Swanson, [mdswanson.com](http://mdswanson.com) [@_swanson](http://twitter.com/_swanson) From 9e359ce191e3bc22e771e48a7d91e7b1c39b9a80 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Sun, 16 Feb 2014 13:34:00 +0100 Subject: [PATCH 0006/1107] Update paragraph about API key --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d51261743..c312f4b00 100644 --- a/README.md +++ b/README.md @@ -94,10 +94,20 @@ Use the following settings: Server: {path-to-stringer}/fever (e.g. http://reader.example.com/fever) Email: stringer (case-sensitive) -Password: {your-stringer-password} +Password: {your-stringer-api-key} ``` -If you have previously setup Stringer, you will need to migrate your database and run `rake change_password` for the API key to be setup properly. +If you are running Stringer revision `0d35ec2` (May 15th 2013) or older, you +will need to migrate your database and run `rake change_password` for the API +key to be setup properly. + +Your API key is the md5 checksum of the string `stringer:{your-stringer-password}`. +Assuming your password is "opensesame", the following command will calculate +your API key for you: + +```sh +echo "stringer:opensesame" | md5sum | cut -d' ' -f1 +``` ### Translations From 2a980346872770c43b6545c199d528ed892307ea Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Tue, 18 Feb 2014 19:24:52 +0100 Subject: [PATCH 0007/1107] Revert "Update paragraph about API key" This reverts commit 9e359ce191e3bc22e771e48a7d91e7b1c39b9a80. The new paragraph is wrong! --- README.md | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c312f4b00..d51261743 100644 --- a/README.md +++ b/README.md @@ -94,20 +94,10 @@ Use the following settings: Server: {path-to-stringer}/fever (e.g. http://reader.example.com/fever) Email: stringer (case-sensitive) -Password: {your-stringer-api-key} +Password: {your-stringer-password} ``` -If you are running Stringer revision `0d35ec2` (May 15th 2013) or older, you -will need to migrate your database and run `rake change_password` for the API -key to be setup properly. - -Your API key is the md5 checksum of the string `stringer:{your-stringer-password}`. -Assuming your password is "opensesame", the following command will calculate -your API key for you: - -```sh -echo "stringer:opensesame" | md5sum | cut -d' ' -f1 -``` +If you have previously setup Stringer, you will need to migrate your database and run `rake change_password` for the API key to be setup properly. ### Translations From 428dd0636bc231bbfe6b55c6ca07528b65d913f6 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Mon, 24 Feb 2014 19:52:45 +0100 Subject: [PATCH 0008/1107] Handle whitespace before closing `>` Fixes #267. --- app/repositories/story_repository.rb | 2 +- spec/repositories/story_repository_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index f90b0d97a..ec86a0835 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -83,7 +83,7 @@ def self.extract_content(entry) end def self.sanitize(content) - Loofah.fragment(content.gsub(//i, "")).scrub!(:prune).to_s + Loofah.fragment(content.gsub(//i, "")).scrub!(:prune).to_s end def self.expand_absolute_urls(content, base_url) diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 4a2843fbd..1cf76bbd4 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -55,7 +55,7 @@ end describe ".extract_content" do - let(:entry) do + let(:entry) do double(url: "http://mdswanson.com", content: "Some test content") end @@ -78,7 +78,7 @@ describe ".sanitize" do context "regressions" do it "handles tag properly" do - result = StoryRepository.sanitize("WM_ERROR asdf") + result = StoryRepository.sanitize("WM_ERROR asdf") result.should eq "WM_ERROR asdf" end From c46d25dc377e7a4cc70c5d14197bbd2444049caf Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Sat, 15 Mar 2014 11:09:08 +0100 Subject: [PATCH 0009/1107] Add missing step in OpenShift docs Fixes #303. --- docs/OpenShift.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/OpenShift.md b/docs/OpenShift.md index 28fca0c5e..ab1bb9b7d 100644 --- a/docs/OpenShift.md +++ b/docs/OpenShift.md @@ -68,6 +68,12 @@ Deploying into OpenShift gem "pry-byebug", "~> 1.2" ``` + After removing the `pry-byebug` gem from `Gemfile`, the bundle has to be updated. + + ```sh + bundle install + ``` + 10. Finally, once completed, all changes should be committed and pushed to OpenShift. Note that it might take a while when pushing to OpenShift. ```sh From 71199cc432fe03ce483e3f7b55cea683c09d6cfc Mon Sep 17 00:00:00 2001 From: Matt Swanson Date: Thu, 27 Mar 2014 21:55:04 -0400 Subject: [PATCH 0010/1107] Filter out unprintable characters in StoryRepository#sanitize, fixes #295 --- app/repositories/story_repository.rb | 5 ++++- spec/repositories/story_repository_spec.rb | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index ec86a0835..144baecc2 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -83,7 +83,10 @@ def self.extract_content(entry) end def self.sanitize(content) - Loofah.fragment(content.gsub(//i, "")).scrub!(:prune).to_s + Loofah.fragment(content.gsub(//i, "")) + .scrub!(:prune) + .to_s + .gsub(/[^[:print:]]/, '') end def self.expand_absolute_urls(content, base_url) diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 1cf76bbd4..5bde2d4c5 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -86,6 +86,11 @@ result = StoryRepository.sanitize("
    some code
    ") result.should eq "
    some code
    " end + + it "handles unprintable characters" do + result = StoryRepository.sanitize("n
") + result.should eq "n" + end end end end From 0f73078ebe9c79b850cfa4f51168c87f0180c700 Mon Sep 17 00:00:00 2001 From: Jon Allured Date: Fri, 11 Apr 2014 16:36:17 -0500 Subject: [PATCH 0011/1107] Ignore the gemset file --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b67af0ebe..fe61e8b53 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.rbc .bundle .config +.ruby-gemset .ruby-version coverage InstalledFiles @@ -21,4 +22,4 @@ bin/ db/*.sqlite .DS_Store -.localeapp \ No newline at end of file +.localeapp From 52b7ff8b49b601a7bfa814f68869d248580d973b Mon Sep 17 00:00:00 2001 From: Jon Allured Date: Fri, 11 Apr 2014 16:50:26 -0500 Subject: [PATCH 0012/1107] Ensure there's a temp folder --- tmp/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tmp/.gitkeep diff --git a/tmp/.gitkeep b/tmp/.gitkeep new file mode 100644 index 000000000..e69de29bb From 2149858e4befbe8d6dd31cd78371f88c3b1fba24 Mon Sep 17 00:00:00 2001 From: Jon Allured Date: Fri, 11 Apr 2014 16:51:11 -0500 Subject: [PATCH 0013/1107] Quiet down that test output --- spec/support/active_record.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/support/active_record.rb b/spec/support/active_record.rb index 52ac93f9f..633be899d 100644 --- a/spec/support/active_record.rb +++ b/spec/support/active_record.rb @@ -2,6 +2,7 @@ config = YAML.load(File.read('config/database.yml')) ActiveRecord::Base.establish_connection(config['test']) +ActiveRecord::Base.logger = Logger.new('tmp/test.log') def need_to_migrate? ActiveRecord::Migrator.new(:up, ActiveRecord::Migrator.migrations('db/migrate')).pending_migrations.any? From 66462b1d1955a668a71be71f5c8a27d019dde2dc Mon Sep 17 00:00:00 2001 From: Jon Allured Date: Fri, 11 Apr 2014 16:57:29 -0500 Subject: [PATCH 0014/1107] Upgrade to Feedjira [closes #307] --- Gemfile | 2 +- Gemfile.lock | 4 ++-- app/tasks/fetch_feed.rb | 4 ++-- app/utils/feed_discovery.rb | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 0b219450c..b64ded4c1 100644 --- a/Gemfile +++ b/Gemfile @@ -35,7 +35,7 @@ gem "bcrypt-ruby", "~> 3.1.2" gem "delayed_job", "~> 4.0" gem "delayed_job_active_record", "~> 4.0" gem "feedbag", "~> 0.9.2" -gem "feedzirra", "~> 0.6.0" +gem "feedjira", "~> 1.2.0" gem "highline", "~> 1.6", ">= 1.6.20", require: false gem "i18n", "~> 0.6.9" gem "loofah", github: "swanson/loofah" diff --git a/Gemfile.lock b/Gemfile.lock index c8cc0411b..fa887f1db 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -60,7 +60,7 @@ GEM i18n (~> 0.5) feedbag (0.9.2) hpricot (>= 0.6) - feedzirra (0.6.0) + feedjira (1.2.0) curb (~> 0.8.1) loofah (~> 1.2.1) sax-machine (~> 0.2.1) @@ -173,7 +173,7 @@ DEPENDENCIES excon (~> 0.31.0) faker (~> 1.2) feedbag (~> 0.9.2) - feedzirra (~> 0.6.0) + feedjira (~> 1.2.0) foreman (~> 0.63.0) formatador (~> 0.2.4) highline (~> 1.6, >= 1.6.20) diff --git a/app/tasks/fetch_feed.rb b/app/tasks/fetch_feed.rb index a2a60e0d1..6c1064e34 100644 --- a/app/tasks/fetch_feed.rb +++ b/app/tasks/fetch_feed.rb @@ -1,4 +1,4 @@ -require "feedzirra" +require "feedjira" require_relative "../repositories/story_repository" require_relative "../repositories/feed_repository" @@ -8,7 +8,7 @@ class FetchFeed USER_AGENT = "Stringer (https://github.com/swanson/stringer)" - def initialize(feed, feed_parser = Feedzirra::Feed, logger = nil) + def initialize(feed, feed_parser = Feedjira::Feed, logger = nil) @feed = feed @parser = feed_parser @logger = logger diff --git a/app/utils/feed_discovery.rb b/app/utils/feed_discovery.rb index fb44ecf35..3b448c911 100644 --- a/app/utils/feed_discovery.rb +++ b/app/utils/feed_discovery.rb @@ -1,8 +1,8 @@ require "feedbag" -require "feedzirra" +require "feedjira" class FeedDiscovery - def discover(url, finder = Feedbag, parser = Feedzirra::Feed) + def discover(url, finder = Feedbag, parser = Feedjira::Feed) get_feed_for_url(url, finder, parser) do urls = finder.find(url) return false if urls.empty? From 4dd44a9173029ecaee405fe1b90d6909cf6cad9c Mon Sep 17 00:00:00 2001 From: Jon Allured Date: Sat, 12 Apr 2014 10:09:39 -0500 Subject: [PATCH 0015/1107] Move test log output to log folder --- .gitignore | 1 + {tmp => log}/.gitkeep | 0 spec/support/active_record.rb | 2 +- 3 files changed, 2 insertions(+), 1 deletion(-) rename {tmp => log}/.gitkeep (100%) diff --git a/.gitignore b/.gitignore index fe61e8b53..0bc0e6d65 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ spec/reports test/tmp test/version_tmp tmp +log # YARD artifacts .yardoc diff --git a/tmp/.gitkeep b/log/.gitkeep similarity index 100% rename from tmp/.gitkeep rename to log/.gitkeep diff --git a/spec/support/active_record.rb b/spec/support/active_record.rb index 633be899d..8054b3e7b 100644 --- a/spec/support/active_record.rb +++ b/spec/support/active_record.rb @@ -2,7 +2,7 @@ config = YAML.load(File.read('config/database.yml')) ActiveRecord::Base.establish_connection(config['test']) -ActiveRecord::Base.logger = Logger.new('tmp/test.log') +ActiveRecord::Base.logger = Logger.new('log/test.log') def need_to_migrate? ActiveRecord::Migrator.new(:up, ActiveRecord::Migrator.migrations('db/migrate')).pending_migrations.any? From 5b2088b3715e84c27b7cd19dc4909c219dd27e20 Mon Sep 17 00:00:00 2001 From: Jon Allured Date: Sat, 12 Apr 2014 14:26:31 -0500 Subject: [PATCH 0016/1107] Update link to Feedjira --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d51261743..150afb04d 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ You can launch an interactive console (ala `rails c`) using `racksh`. ## Acknowledgements -Most of the heavy-lifting is done by [`feedzirra`](https://github.com/pauldix/feedzirra) and [`feedbag`](https://github.com/dwillis/feedbag). +Most of the heavy-lifting is done by [`feedjira`](https://github.com/feedjira/feedjira) and [`feedbag`](https://github.com/dwillis/feedbag). General sexiness courtesy of [`Twitter Bootstrap`](http://twitter.github.io/bootstrap/) and [`Flat UI`](http://designmodo.github.io/Flat-UI/). From e12a7630efbd020b519b00cb43004458ff79f316 Mon Sep 17 00:00:00 2001 From: Dan Boger Date: Fri, 18 Apr 2014 17:50:53 -0700 Subject: [PATCH 0017/1107] Add a unique secret token to install instructions Failing to do so means that if you have multiple stringer installs on the same host, one password will grant you access to all of them. --- docs/VPS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/VPS.md b/docs/VPS.md index a51943338..ebb6a3aa5 100644 --- a/docs/VPS.md +++ b/docs/VPS.md @@ -89,6 +89,7 @@ Stringer uses environment variables to determine information about your database echo 'export STRINGER_DATABASE_USERNAME="stringer"' >> $HOME/.bash_profile echo 'export STRINGER_DATABASE_PASSWORD="EDIT_ME"' >> $HOME/.bash_profile echo 'export RACK_ENV="production"' >> $HOME/.bash_profile + echo 'export SECRET_TOKEN="$$$RANDOM"` >> $HOME/.bash_profile source ~/.bash_profile Tell stringer to run the database in production mode, using the postgres database you created earlier. From d7fbad89cd3936c73fbd10f54ebce722ae6a238d Mon Sep 17 00:00:00 2001 From: Dan Boger Date: Sat, 19 Apr 2014 14:52:24 -0700 Subject: [PATCH 0018/1107] Update keyboard shortcut screenshots --- screenshots/keyboard_shortcuts.png | Bin 43635 -> 22507 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/screenshots/keyboard_shortcuts.png b/screenshots/keyboard_shortcuts.png index 6cabc8b546af1b1c813afa66741f261ec8fc85cc..7a3019fe2da7671adc1435c1e72b3c9517dc40a3 100644 GIT binary patch literal 22507 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 Z@J_LxHjXVAt6v9 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 dd+)X%9aku?7};5=Z=wKymvoJFs Date: Mon, 21 Apr 2014 20:13:59 -0400 Subject: [PATCH 0019/1107] Loosen unprintable character sanitization We were too aggressive with our fixing of #295. [:print:] is catching \r\n, so let's explicitly fix the weird JSON unicode chars (this aligns with the rack fix as well: https://github.com/rack/rack-contrib/pull/37/files). --- app/repositories/story_repository.rb | 3 ++- spec/repositories/story_repository_spec.rb | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index 144baecc2..bffbe1e7c 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -86,7 +86,8 @@ def self.sanitize(content) Loofah.fragment(content.gsub(//i, "")) .scrub!(:prune) .to_s - .gsub(/[^[:print:]]/, '') + .gsub("\u2028", '') + .gsub("\u2029", '') end def self.expand_absolute_urls(content, base_url) diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 5bde2d4c5..4c04ed06b 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -88,9 +88,14 @@ end it "handles unprintable characters" do - result = StoryRepository.sanitize("n
") + result = StoryRepository.sanitize("n\u2028\u2029") result.should eq "n" end + + it "preserves line endings" do + result = StoryRepository.sanitize("test\r\ncase") + result.should eq "test\r\ncase" + end end end end From e4217e4fd83d4a5b53ba3a459c7ec7cb527cfe1b Mon Sep 17 00:00:00 2001 From: Matt Swanson Date: Mon, 21 Apr 2014 18:50:48 -0400 Subject: [PATCH 0020/1107] Add migration to correct unicode issues that were resolved by #295 --- db/migrate/20140421224454_fix_invalid_unicode.rb | 12 ++++++++++++ db/schema.rb | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20140421224454_fix_invalid_unicode.rb diff --git a/db/migrate/20140421224454_fix_invalid_unicode.rb b/db/migrate/20140421224454_fix_invalid_unicode.rb new file mode 100644 index 000000000..f32fde2eb --- /dev/null +++ b/db/migrate/20140421224454_fix_invalid_unicode.rb @@ -0,0 +1,12 @@ +class FixInvalidUnicode < ActiveRecord::Migration + def up + Story.find_each do |story| + valid_body = story.body.gsub("\u2028", '').gsub("\u2029", '') + story.update_attribute(:body, valid_body) + end + end + + def down + # skip + end +end diff --git a/db/schema.rb b/db/schema.rb index fdd01f821..5d23f2026 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20130905204142) do +ActiveRecord::Schema.define(version: 20140421224454) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From e83e66dab1cd8137c812ccc19e78cd48141ecd4b Mon Sep 17 00:00:00 2001 From: Matt Swanson Date: Tue, 22 Apr 2014 20:50:25 -0400 Subject: [PATCH 0021/1107] Fix CSS for keyboard shortcut modal --- app/public/css/styles.css | 5 +++++ screenshots/keyboard_shortcuts.png | Bin 22507 -> 49791 bytes 2 files changed, 5 insertions(+) diff --git a/app/public/css/styles.css b/app/public/css/styles.css index 4e69b86db..82c16d3c6 100644 --- a/app/public/css/styles.css +++ b/app/public/css/styles.css @@ -550,6 +550,11 @@ kbd { white-space: nowrap; } + +#shortcuts .modal-body { + max-height: 500px; +} + ul.shortcut-legend li { margin-bottom: 10px; } diff --git a/screenshots/keyboard_shortcuts.png b/screenshots/keyboard_shortcuts.png index 7a3019fe2da7671adc1435c1e72b3c9517dc40a3..d5c26bb7ef61209b6a63d7fde7a8134f57ac8719 100644 GIT binary patch literal 49791 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 Date: Wed, 14 May 2014 11:41:27 -0400 Subject: [PATCH 0022/1107] Bump feedjira and loofah versions --- Gemfile | 4 ++-- Gemfile.lock | 17 ++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/Gemfile b/Gemfile index b64ded4c1..56c228de9 100644 --- a/Gemfile +++ b/Gemfile @@ -35,10 +35,10 @@ gem "bcrypt-ruby", "~> 3.1.2" gem "delayed_job", "~> 4.0" gem "delayed_job_active_record", "~> 4.0" gem "feedbag", "~> 0.9.2" -gem "feedjira", "~> 1.2.0" +gem "feedjira", "~> 1.3.0" gem "highline", "~> 1.6", ">= 1.6.20", require: false gem "i18n", "~> 0.6.9" -gem "loofah", github: "swanson/loofah" +gem "loofah", "~> 2.0.0" gem "nokogiri", "~> 1.6" gem "racksh", "~> 1.0" gem "rake", "~> 10.1", ">= 10.1.1" diff --git a/Gemfile.lock b/Gemfile.lock index fa887f1db..e45c2a291 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,13 +5,6 @@ GIT specs: arel (4.0.1.20131022201058) -GIT - remote: git://github.com/swanson/loofah.git - revision: 825d715e6f1281501882d886cf34e82aebabb356 - specs: - loofah (1.2.1.20130718080038) - nokogiri (>= 1.5.9) - GEM remote: https://rubygems.org/ specs: @@ -60,9 +53,9 @@ GEM i18n (~> 0.5) feedbag (0.9.2) hpricot (>= 0.6) - feedjira (1.2.0) + feedjira (1.3.0) curb (~> 0.8.1) - loofah (~> 1.2.1) + loofah (~> 2.0.0) sax-machine (~> 0.2.1) foreman (0.63.0) dotenv (>= 0.7) @@ -73,6 +66,8 @@ GEM i18n (0.6.9) jsmin (1.0.1) kgio (2.8.1) + loofah (2.0.0) + nokogiri (>= 1.5.9) method_source (0.8.2) mime-types (2.0) mini_portile (0.5.2) @@ -173,12 +168,12 @@ DEPENDENCIES excon (~> 0.31.0) faker (~> 1.2) feedbag (~> 0.9.2) - feedjira (~> 1.2.0) + feedjira (~> 1.3.0) foreman (~> 0.63.0) formatador (~> 0.2.4) highline (~> 1.6, >= 1.6.20) i18n (~> 0.6.9) - loofah! + loofah (~> 2.0.0) netrc (~> 0.7.7) nokogiri (~> 1.6) pg (~> 0.17.1) From 5102bb6b3a595a764de010c721c59736a6be3295 Mon Sep 17 00:00:00 2001 From: Matt Swanson Date: Wed, 14 May 2014 11:44:35 -0400 Subject: [PATCH 0023/1107] Replace unprintable unicode character workaround with new loofah scrubber --- app/repositories/story_repository.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index bffbe1e7c..9e581c2f7 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -85,9 +85,8 @@ def self.extract_content(entry) def self.sanitize(content) Loofah.fragment(content.gsub(//i, "")) .scrub!(:prune) + .scrub!(:unprintable) .to_s - .gsub("\u2028", '') - .gsub("\u2029", '') end def self.expand_absolute_urls(content, base_url) From 7e9905d6ecac568cac73791c07f0eea778e2a5d7 Mon Sep 17 00:00:00 2001 From: matt swanson Date: Sun, 18 May 2014 14:05:32 -0400 Subject: [PATCH 0024/1107] Remove WIP --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 150afb04d..35b94125f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Code Climate](https://codeclimate.com/github/swanson/stringer.png)](https://codeclimate.com/github/swanson/stringer) [![Coverage Status](https://coveralls.io/repos/swanson/stringer/badge.png?branch=master)](https://coveralls.io/r/swanson/stringer) -### A [work-in-progress] self-hosted, anti-social RSS reader. +### A self-hosted, anti-social RSS reader. Stringer has no external dependencies, no social recommendations/sharing, and no fancy machine learning algorithms. From 90efedacbb36e0d10737e76670c94bef4b95ba89 Mon Sep 17 00:00:00 2001 From: matt swanson Date: Sun, 18 May 2014 14:18:40 -0400 Subject: [PATCH 0025/1107] Add Victor to contact section --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 35b94125f..791c46d8d 100644 --- a/README.md +++ b/README.md @@ -147,3 +147,4 @@ General sexiness courtesy of [`Twitter Bootstrap`](http://twitter.github.io/boot ## Contact Matt Swanson, [mdswanson.com](http://mdswanson.com) [@_swanson](http://twitter.com/_swanson) +Victor Koronen, [victor.koronen.se](http://victor.koronen.se/), [@victorkoronen](https://twitter.com/victorkoronen) From 0a013ab03994896d659ec9bc6c45958d4b0c8a33 Mon Sep 17 00:00:00 2001 From: matt swanson Date: Sun, 18 May 2014 14:19:26 -0400 Subject: [PATCH 0026/1107] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 791c46d8d..7e24b2130 100644 --- a/README.md +++ b/README.md @@ -147,4 +147,5 @@ General sexiness courtesy of [`Twitter Bootstrap`](http://twitter.github.io/boot ## Contact Matt Swanson, [mdswanson.com](http://mdswanson.com) [@_swanson](http://twitter.com/_swanson) + Victor Koronen, [victor.koronen.se](http://victor.koronen.se/), [@victorkoronen](https://twitter.com/victorkoronen) From 0af75277ce6b5788875b53a32e54cc6d217bb993 Mon Sep 17 00:00:00 2001 From: matt swanson Date: Sun, 18 May 2014 14:19:59 -0400 Subject: [PATCH 0027/1107] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e24b2130..e2e5358b8 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,6 @@ General sexiness courtesy of [`Twitter Bootstrap`](http://twitter.github.io/boot ## Contact -Matt Swanson, [mdswanson.com](http://mdswanson.com) [@_swanson](http://twitter.com/_swanson) +Matt Swanson, [mdswanson.com](http://mdswanson.com), [@_swanson](http://twitter.com/_swanson) Victor Koronen, [victor.koronen.se](http://victor.koronen.se/), [@victorkoronen](https://twitter.com/victorkoronen) From 0dc67db6489c01df1fc911b073c6a5f1490aba56 Mon Sep 17 00:00:00 2001 From: Dan Boger Date: Mon, 19 May 2014 00:36:58 -0700 Subject: [PATCH 0028/1107] Use openssl for the random token --- docs/VPS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/VPS.md b/docs/VPS.md index ebb6a3aa5..b1a787dec 100644 --- a/docs/VPS.md +++ b/docs/VPS.md @@ -89,7 +89,7 @@ Stringer uses environment variables to determine information about your database echo 'export STRINGER_DATABASE_USERNAME="stringer"' >> $HOME/.bash_profile echo 'export STRINGER_DATABASE_PASSWORD="EDIT_ME"' >> $HOME/.bash_profile echo 'export RACK_ENV="production"' >> $HOME/.bash_profile - echo 'export SECRET_TOKEN="$$$RANDOM"` >> $HOME/.bash_profile + echo "export SECRET_TOKEN=`openssl rand -hex 20`" >> $HOME/.bash_profile source ~/.bash_profile Tell stringer to run the database in production mode, using the postgres database you created earlier. From b2907b883a389aab8a09bb1bb10b49cdf4b7e4c8 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Mon, 26 May 2014 20:12:21 +0200 Subject: [PATCH 0029/1107] Add a simple feed edit view --- app/controllers/feeds_controller.rb | 10 ++++++++++ app/public/css/styles.css | 14 +++++++++++++- app/views/feeds/edit.erb | 17 +++++++++++++++++ app/views/partials/_feed.erb | 11 +++++++---- config/locales/en.yml | 4 ++++ spec/controllers/feeds_controller_spec.rb | 12 ++++++++++++ 6 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 app/views/feeds/edit.erb diff --git a/app/controllers/feeds_controller.rb b/app/controllers/feeds_controller.rb index 3b6ae5662..0f34b56ee 100644 --- a/app/controllers/feeds_controller.rb +++ b/app/controllers/feeds_controller.rb @@ -9,6 +9,12 @@ class Stringer < Sinatra::Base erb :'feeds/index' end + get "/feeds/:id/edit" do + @feed = FeedRepository.fetch(params[:id]) + + erb :'feeds/edit' + end + delete "/feeds/:feed_id" do FeedRepository.delete(params[:feed_id]) @@ -47,6 +53,10 @@ class Stringer < Sinatra::Base redirect to("/setup/tutorial") end + post "/feeds/:id" do + redirect to('/feeds') + end + get "/feeds/export" do content_type 'application/xml' attachment 'stringer.opml' diff --git a/app/public/css/styles.css b/app/public/css/styles.css index 82c16d3c6..f4e97da8a 100644 --- a/app/public/css/styles.css +++ b/app/public/css/styles.css @@ -362,6 +362,18 @@ li.feed .last-updated-time { display: inline-block; } +li.feed .edit-feed { + cursor: pointer; +} + +li.feed .edit-feed a { + text-align: right; + padding-left: 3px; + padding-right: 3px; + margin-left: 5px; + color: #000; +} + li.feed .remove-feed { cursor: pointer; } @@ -370,7 +382,7 @@ li.feed .remove-feed a { text-align: center; padding-left: 3px; padding-right: 3px; - margin-left: 10px; + margin-left: 5px; color: #C0392B; } diff --git a/app/views/feeds/edit.erb b/app/views/feeds/edit.erb new file mode 100644 index 000000000..ef4e68c20 --- /dev/null +++ b/app/views/feeds/edit.erb @@ -0,0 +1,17 @@ +
    + <%= render_partial :feed_action_bar %> +
    + +
    +

    <%= @feed.name %>

    +
    +
    + +
    + + + +
    + +
    +
    diff --git a/app/views/partials/_feed.erb b/app/views/partials/_feed.erb index 8565a3f71..99d8086da 100644 --- a/app/views/partials/_feed.erb +++ b/app/views/partials/_feed.erb @@ -11,7 +11,7 @@

    -
    +
    <%= t('partials.feed.last_updated') %> <% if feed.last_fetched %> @@ -21,13 +21,16 @@ <% end %>
    -
    +
    - + + + "> + - +
    diff --git a/config/locales/en.yml b/config/locales/en.yml index 0b56de2ae..c6521ca19 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -33,6 +33,10 @@ en: index: add: add add_some_feeds: Hey, you should %{add} some feeds. + edit: + fields: + feed_url: Feed URL + submit: Update first_run: password: anti_social: anti-social diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index ef08e74d7..98f5f074c 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -26,6 +26,18 @@ end end + describe "GET /feeds/:feed_id/edit" do + it "fetches a feed given the id" do + feed = Feed.new(name: 'Rainbows and unicorns', url: 'example.com/feed') + FeedRepository.should_receive(:fetch).with("123").and_return(feed) + + get "/feeds/123/edit" + + last_response.body.should include('Rainbows and unicorns') + last_response.body.should include('example.com/feed') + end + end + describe "DELETE /feeds/:feed_id" do it "deletes a feed given the id" do FeedRepository.should_receive(:delete).with("123") From f7b5d92a602f6b893f7aabd92603e65102a1b8b5 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Mon, 26 May 2014 20:47:36 +0200 Subject: [PATCH 0030/1107] Implement a feed update action --- app/controllers/feeds_controller.rb | 4 ++++ app/repositories/feed_repository.rb | 5 +++++ spec/controllers/feeds_controller_spec.rb | 12 ++++++++++++ spec/repositories/feed_repository_spec.rb | 12 +++++++++++- 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/app/controllers/feeds_controller.rb b/app/controllers/feeds_controller.rb index 0f34b56ee..a50c6be88 100644 --- a/app/controllers/feeds_controller.rb +++ b/app/controllers/feeds_controller.rb @@ -54,6 +54,10 @@ class Stringer < Sinatra::Base end post "/feeds/:id" do + feed = FeedRepository.fetch(params[:id]) + + FeedRepository.update_url(feed, params[:feed_url]) + redirect to('/feeds') end diff --git a/app/repositories/feed_repository.rb b/app/repositories/feed_repository.rb index e8bf2f424..272074ecd 100644 --- a/app/repositories/feed_repository.rb +++ b/app/repositories/feed_repository.rb @@ -11,6 +11,11 @@ def self.fetch_by_ids(ids) Feed.where(id: ids) end + def self.update_url(feed, url) + feed.url = url + feed.save + end + def self.update_last_fetched(feed, timestamp) if self.valid_timestamp?(timestamp, feed.last_fetched) feed.last_fetched = timestamp diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index 98f5f074c..76ee0539b 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -38,6 +38,18 @@ end end + describe "POST /feeds/:feed_id" do + it "updates a feed given the id" do + feed = FeedFactory.build(url: 'example.com/atom') + FeedRepository.should_receive(:fetch).with("123").and_return(feed) + FeedRepository.should_receive(:update_url).with(feed, 'example.com/feed') + + post "/feeds/123", feed_id: "123", feed_url: "example.com/feed" + + last_response.should be_redirect + end + end + describe "DELETE /feeds/:feed_id" do it "deletes a feed given the id" do FeedRepository.should_receive(:delete).with("123") diff --git a/spec/repositories/feed_repository_spec.rb b/spec/repositories/feed_repository_spec.rb index 6ceedd76d..b0ce62d1a 100644 --- a/spec/repositories/feed_repository_spec.rb +++ b/spec/repositories/feed_repository_spec.rb @@ -43,6 +43,16 @@ end end + describe ".update_url" do + it "saves the url" do + feed = Feed.new + + FeedRepository.update_url(feed, 'example.com/feed') + + feed.url.should eq 'example.com/feed' + end + end + describe "fetch" do let(:feed) { Feed.new(id: 1) } @@ -59,4 +69,4 @@ result.should eq feed end end -end \ No newline at end of file +end From 5c52b378edf4664d1c75d43f38e63b5f0369dc25 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Mon, 26 May 2014 21:03:38 +0200 Subject: [PATCH 0031/1107] Add a flash message on successful update --- app/controllers/feeds_controller.rb | 1 + config/locales/en.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/controllers/feeds_controller.rb b/app/controllers/feeds_controller.rb index a50c6be88..58bf69e7e 100644 --- a/app/controllers/feeds_controller.rb +++ b/app/controllers/feeds_controller.rb @@ -58,6 +58,7 @@ class Stringer < Sinatra::Base FeedRepository.update_url(feed, params[:feed_url]) + flash[:success] = t('feeds.edit.flash.updated_successfully') redirect to('/feeds') end diff --git a/config/locales/en.yml b/config/locales/en.yml index c6521ca19..565e7be53 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -37,6 +37,8 @@ en: fields: feed_url: Feed URL submit: Update + flash: + updated_successfully: Updated the feed URL for ya'! first_run: password: anti_social: anti-social From 5437a379fb720a2955cfb6116572857ecce12c10 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Tue, 27 May 2014 09:53:34 +0200 Subject: [PATCH 0032/1107] Use PUT to update a feed --- app.rb | 1 + app/controllers/feeds_controller.rb | 18 +++++++++--------- app/views/feeds/edit.erb | 1 + spec/controllers/feeds_controller_spec.rb | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app.rb b/app.rb index f68db4565..f36ee73b5 100644 --- a/app.rb +++ b/app.rb @@ -29,6 +29,7 @@ class Stringer < Sinatra::Base enable :sessions set :session_secret, ENV["SECRET_TOKEN"] || "secret!" enable :logging + enable :method_override ActiveRecord::Base.include_root_in_json = false end diff --git a/app/controllers/feeds_controller.rb b/app/controllers/feeds_controller.rb index 58bf69e7e..8670d0a95 100644 --- a/app/controllers/feeds_controller.rb +++ b/app/controllers/feeds_controller.rb @@ -15,6 +15,15 @@ class Stringer < Sinatra::Base erb :'feeds/edit' end + put "/feeds/:id" do + feed = FeedRepository.fetch(params[:id]) + + FeedRepository.update_url(feed, params[:feed_url]) + + flash[:success] = t('feeds.edit.flash.updated_successfully') + redirect to('/feeds') + end + delete "/feeds/:feed_id" do FeedRepository.delete(params[:feed_id]) @@ -53,15 +62,6 @@ class Stringer < Sinatra::Base redirect to("/setup/tutorial") end - post "/feeds/:id" do - feed = FeedRepository.fetch(params[:id]) - - FeedRepository.update_url(feed, params[:feed_url]) - - flash[:success] = t('feeds.edit.flash.updated_successfully') - redirect to('/feeds') - end - get "/feeds/export" do content_type 'application/xml' attachment 'stringer.opml' diff --git a/app/views/feeds/edit.erb b/app/views/feeds/edit.erb index ef4e68c20..2bc7674e7 100644 --- a/app/views/feeds/edit.erb +++ b/app/views/feeds/edit.erb @@ -6,6 +6,7 @@

    <%= @feed.name %>


    +
    diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index 76ee0539b..c1f7ab2b1 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -38,13 +38,13 @@ end end - describe "POST /feeds/:feed_id" do + describe "PUT /feeds/:feed_id" do it "updates a feed given the id" do feed = FeedFactory.build(url: 'example.com/atom') FeedRepository.should_receive(:fetch).with("123").and_return(feed) FeedRepository.should_receive(:update_url).with(feed, 'example.com/feed') - post "/feeds/123", feed_id: "123", feed_url: "example.com/feed" + put "/feeds/123", feed_id: "123", feed_url: "example.com/feed" last_response.should be_redirect end From f2a76e5ffb41169948f666736cc162b5fd302191 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Tue, 27 May 2014 09:54:57 +0200 Subject: [PATCH 0033/1107] Copy: Update -> Save --- config/locales/en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 565e7be53..b430c17b0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -36,7 +36,7 @@ en: edit: fields: feed_url: Feed URL - submit: Update + submit: Save flash: updated_successfully: Updated the feed URL for ya'! first_run: From 1545bdb3a8ad515b34157fe0bb0ca8437f853c49 Mon Sep 17 00:00:00 2001 From: Damir Gainetdinov Date: Tue, 15 Apr 2014 09:58:26 +0400 Subject: [PATCH 0034/1107] Add groups for feeds in Fever API --- app/commands/feeds/import_from_opml.rb | 33 ++++++-- app/commands/stories/mark_group_as_read.rb | 12 ++- app/fever_api/read_feeds_groups.rb | 12 +-- app/fever_api/read_groups.rb | 13 +-- app/models/feed.rb | 1 + app/models/group.rb | 7 ++ app/repositories/feed_repository.rb | 4 + app/repositories/group_repository.rb | 7 ++ app/repositories/story_repository.rb | 6 +- app/utils/opml_parser.rb | 30 ++++--- ..._groups_table_and_foreign_keys_to_feeds.rb | 14 ++++ db/schema.rb | 7 ++ spec/commands/feeds/add_new_feed_spec.rb | 2 +- spec/commands/feeds/import_from_opml_spec.rb | 82 +++++++++++++++++++ .../stories/mark_group_as_read_spec.rb | 40 ++++++--- spec/factories/feed_factory.rb | 1 + spec/factories/group_factory.rb | 16 ++++ spec/fever_api/read_feeds_groups_spec.rb | 8 +- spec/fever_api/read_groups_spec.rb | 15 +++- spec/fever_api_spec.rb | 53 +++++++----- spec/spec_helper.rb | 1 + spec/support/files/subscriptions.xml | 27 ++++++ spec/utils/opml_parser_spec.rb | 29 ++++--- 23 files changed, 330 insertions(+), 90 deletions(-) create mode 100644 app/models/group.rb create mode 100644 app/repositories/group_repository.rb create mode 100644 db/migrate/20140413100725_add_groups_table_and_foreign_keys_to_feeds.rb create mode 100644 spec/commands/feeds/import_from_opml_spec.rb create mode 100644 spec/factories/group_factory.rb create mode 100755 spec/support/files/subscriptions.xml diff --git a/app/commands/feeds/import_from_opml.rb b/app/commands/feeds/import_from_opml.rb index 120a1d5d5..7143917ba 100644 --- a/app/commands/feeds/import_from_opml.rb +++ b/app/commands/feeds/import_from_opml.rb @@ -1,16 +1,35 @@ require_relative "../../models/feed" +require_relative "../../models/group" require_relative "../../utils/opml_parser" class ImportFromOpml ONE_DAY = 24 * 60 * 60 - def self.import(opml_contents) - feeds = OpmlParser.new.parse_feeds(opml_contents) + class << self + def import(opml_contents) + feeds_with_groups = OpmlParser.new.parse_feeds(opml_contents) - feeds.each do |feed| - Feed.create(name: feed[:name], - url: feed[:url], - last_fetched: Time.now - ONE_DAY) + # It considers a situation when feeds are already imported without groups, + # so it's possible to re-import the same subscriptions.xml just to set group_id + # for existing feeds. Feeds without groups are in 'Ungrouped' group, we don't + # create such group and create such feeds with group_id = nil. + # + feeds_with_groups.each do |group_name, parsed_feeds| + if parsed_feeds.size > 0 + group = Group.where(name: group_name).first_or_create unless group_name == 'Ungrouped' + + parsed_feeds.each { |parsed_feed| create_feed(parsed_feed, group) } + end + end + end + + private + + def create_feed(parsed_feed, group) + feed = Feed.where(name: parsed_feed[:name], url: parsed_feed[:url]).first_or_initialize + feed.last_fetched = Time.now - ONE_DAY if feed.new_record? + feed.group_id = group.id if group + feed.save end end -end \ No newline at end of file +end diff --git a/app/commands/stories/mark_group_as_read.rb b/app/commands/stories/mark_group_as_read.rb index 3c379edd5..9269e2c6b 100644 --- a/app/commands/stories/mark_group_as_read.rb +++ b/app/commands/stories/mark_group_as_read.rb @@ -1,18 +1,22 @@ require_relative "../../repositories/story_repository" class MarkGroupAsRead - KINDLING_GROUP_ID = 1 - SPARKS_AND_KINDLING_GROUP_ID = 0 + KINDLING_GROUP_ID = 0 + SPARKS_GROUP_ID = -1 def initialize(group_id, timestamp, repository = StoryRepository) - @group_id = group_id.to_i + @group_id = group_id @repo = repository @timestamp = timestamp end def mark_group_as_read - if [SPARKS_AND_KINDLING_GROUP_ID, KINDLING_GROUP_ID].include? @group_id + return unless @group_id + + if [KINDLING_GROUP_ID, SPARKS_GROUP_ID].include?(@group_id.to_i) @repo.fetch_unread_by_timestamp(@timestamp).update_all(is_read: true) + elsif @group_id.to_i > 0 + @repo.fetch_unread_by_timestamp_and_group(@timestamp, @group_id).update_all(is_read: true) end end end diff --git a/app/fever_api/read_feeds_groups.rb b/app/fever_api/read_feeds_groups.rb index 8ca468b3e..cab9dc954 100644 --- a/app/fever_api/read_feeds_groups.rb +++ b/app/fever_api/read_feeds_groups.rb @@ -17,16 +17,12 @@ def call(params = {}) private def feeds_groups - [ + @feed_repository.in_group.order('LOWER(name)').group_by(&:group_id).map do |group_id, feeds| { - group_id: 1, - feed_ids: feeds.map{|f| f.id}.join(",") + group_id: group_id, + feed_ids: feeds.map(&:id).join(',') } - ] - end - - def feeds - @feed_repository.list + end end end end diff --git a/app/fever_api/read_groups.rb b/app/fever_api/read_groups.rb index 4291baf78..7386252f4 100644 --- a/app/fever_api/read_groups.rb +++ b/app/fever_api/read_groups.rb @@ -1,5 +1,11 @@ +require_relative "../repositories/group_repository" + module FeverAPI class ReadGroups + def initialize(options = {}) + @group_repository = options.fetch(:group_repository){ GroupRepository } + end + def call(params = {}) if params.keys.include?('groups') { groups: groups } @@ -11,12 +17,7 @@ def call(params = {}) private def groups - [ - { - id: 1, - title: "All items" - } - ] + @group_repository.list.map(&:as_fever_json) end end end diff --git a/app/models/feed.rb b/app/models/feed.rb index e9bc2d4c9..006dcb9d9 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -1,5 +1,6 @@ class Feed < ActiveRecord::Base has_many :stories, -> {order "published desc"} , dependent: :delete_all + belongs_to :group validates_uniqueness_of :url diff --git a/app/models/group.rb b/app/models/group.rb new file mode 100644 index 000000000..61b04aa07 --- /dev/null +++ b/app/models/group.rb @@ -0,0 +1,7 @@ +class Group < ActiveRecord::Base + has_many :feeds + + def as_fever_json + { id: self.id, title: self.name } + end +end diff --git a/app/repositories/feed_repository.rb b/app/repositories/feed_repository.rb index e8bf2f424..86749b38b 100644 --- a/app/repositories/feed_repository.rb +++ b/app/repositories/feed_repository.rb @@ -31,6 +31,10 @@ def self.list Feed.order('lower(name)') end + def self.in_group + Feed.where('group_id IS NOT NULL') + end + private def self.valid_timestamp?(new_timestamp, current_timestamp) diff --git a/app/repositories/group_repository.rb b/app/repositories/group_repository.rb new file mode 100644 index 000000000..66dd51409 --- /dev/null +++ b/app/repositories/group_repository.rb @@ -0,0 +1,7 @@ +require_relative "../models/group" + +class GroupRepository + def self.list + Group.order('LOWER(name)') + end +end diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index 9e581c2f7..04f0cbf8a 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -23,7 +23,11 @@ def self.fetch_by_ids(ids) def self.fetch_unread_by_timestamp(timestamp) timestamp = Time.at(timestamp.to_i) - Story.where("created_at < ? AND is_read = ?", timestamp, false) + Story.where('stories.created_at < ?', timestamp).where(is_read: false) + end + + def self.fetch_unread_by_timestamp_and_group(timestamp, group_id) + fetch_unread_by_timestamp(timestamp).joins(:feed).where(feeds: { group_id: group_id }) end def self.fetch_unread_for_feed_by_timestamp(feed_id, timestamp) diff --git a/app/utils/opml_parser.rb b/app/utils/opml_parser.rb index c98a924ed..118f342f5 100644 --- a/app/utils/opml_parser.rb +++ b/app/utils/opml_parser.rb @@ -4,23 +4,29 @@ class OpmlParser def parse_feeds(contents) doc = Nokogiri.XML(contents) - doc.xpath("//body//outline").inject([]) do |feeds, outline| - next feeds if missing_fields? outline.attributes + feeds_with_groups = Hash.new { |h,k| h[k] = [] } - feeds << { - name: extract_name(outline.attributes).value, - url: outline.attributes["xmlUrl"].value - } + doc.xpath('//body/outline').each do |outline| + + if outline.attributes['xmlUrl'].nil? # it's a group! + group_name = extract_name(outline.attributes).value + feeds = outline.xpath('./outline') + else # it's a top-level feed, which means it's a feed without group + group_name = 'Ungrouped' + feeds = [outline] + end + + feeds.each do |feed| + feeds_with_groups[group_name] << { name: extract_name(feed.attributes).value, + url: feed.attributes['xmlUrl'].value } + end end + feeds_with_groups end private - def missing_fields?(attributes) - attributes["xmlUrl"].nil? || - (attributes["title"].nil? && attributes["text"].nil?) - end def extract_name(attributes) - attributes["title"] || attributes["text"] + attributes['title'] || attributes['text'] end -end \ No newline at end of file +end diff --git a/db/migrate/20140413100725_add_groups_table_and_foreign_keys_to_feeds.rb b/db/migrate/20140413100725_add_groups_table_and_foreign_keys_to_feeds.rb new file mode 100644 index 000000000..1ff359975 --- /dev/null +++ b/db/migrate/20140413100725_add_groups_table_and_foreign_keys_to_feeds.rb @@ -0,0 +1,14 @@ +class AddGroupsTableAndForeignKeysToFeeds < ActiveRecord::Migration + def up + create_table :groups do |t| + t.string :name, null: false + t.timestamps null: false + end + add_column :feeds, :group_id, :integer + end + + def down + drop_table :groups + remove_column :feeds, :group_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 5d23f2026..18ff40d37 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -38,10 +38,17 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "status" + t.integer "group_id" end add_index "feeds", ["url"], name: "index_feeds_on_url", unique: true, using: :btree + create_table "groups", force: true do |t| + t.string "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "stories", force: true do |t| t.text "title" t.text "permalink" diff --git a/spec/commands/feeds/add_new_feed_spec.rb b/spec/commands/feeds/add_new_feed_spec.rb index 00d9ff054..e7243db3b 100644 --- a/spec/commands/feeds/add_new_feed_spec.rb +++ b/spec/commands/feeds/add_new_feed_spec.rb @@ -19,7 +19,7 @@ let(:discoverer) { double(discover: feed_result) } let(:feed) { FeedFactory.build } let(:repo) { double } - + it "parses and creates the feed if discovered" do repo.should_receive(:create).and_return(feed) diff --git a/spec/commands/feeds/import_from_opml_spec.rb b/spec/commands/feeds/import_from_opml_spec.rb new file mode 100644 index 000000000..289441078 --- /dev/null +++ b/spec/commands/feeds/import_from_opml_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +app_require "commands/feeds/import_from_opml" + +describe ImportFromOpml do + let(:subscriptions) { File.open(File.expand_path('../../../support/files/subscriptions.xml', __FILE__)) } + + def import + described_class.import(subscriptions) + end + + after do + Feed.delete_all + Group.delete_all + end + + let(:group_1 ) { Group.find_by_name('Football News') } + let(:group_2 ) { Group.find_by_name('RoR') } + + context 'adding group_id for existing feeds' do + let!(:feed_1) { Feed.create(name: 'TMW Football Transfer News', + url: 'http://www.transfermarketweb.com/rss') } + let!(:feed_2) { Feed.create(name: 'GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS - Home', + url: 'http://feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots') } + before { import } + + it 'retains exising feeds' do + feed_1.should be_valid + feed_2.should be_valid + end + + it 'creates new groups' do + group_1.should be + group_2.should be + end + + it 'sets group_id for existing feeds' do + feed_1.reload.group.should eq group_1 + feed_2.reload.group.should eq group_2 + end + end + + context 'creates new feeds with groups' do + let(:feed_1) { Feed.where(name: 'TMW Football Transfer News', + url: 'http://www.transfermarketweb.com/rss') } + + let(:feed_2) { Feed.where(name: 'GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS - Home', + url: 'http://feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots') } + before { import } + + it 'creates groups' do + group_1.should be + group_1.should be + end + + it 'creates feeds' do + feed_1.should exist + feed_2.should exist + end + + it 'sets group' do + feed_1.first.group.should eq group_1 + feed_2.first.group.should eq group_2 + end + end + + context 'creates new feeds without group' do + let(:feed_1) { Feed.where(name: 'Autoblog', url: 'http://feeds.autoblog.com/weblogsinc/autoblog/').first } + let(:feed_2) { Feed.where(name: 'City Guide News', url: 'http://www.probki.net/news/RSS_news_feed.asp').first } + + before { import } + + it 'does not create any new group for feeds without group' do + Group.where('id NOT IN (?)', [group_1.id, group_2.id]).count.should eq 0 + end + + it 'creates feeds without group_id' do + feed_1.group_id.should be_nil + feed_2.group_id.should be_nil + end + end +end diff --git a/spec/commands/stories/mark_group_as_read_spec.rb b/spec/commands/stories/mark_group_as_read_spec.rb index 36deb3c70..dd12ffcdb 100644 --- a/spec/commands/stories/mark_group_as_read_spec.rb +++ b/spec/commands/stories/mark_group_as_read_spec.rb @@ -3,26 +3,44 @@ app_require "commands/stories/mark_group_as_read" describe MarkGroupAsRead do - describe "#mark_group_as_read" do + describe '#mark_group_as_read' do let(:stories) { double } - let(:repo){ double(fetch_unread_by_timestamp: stories) } + let(:repo) { double } + let(:timestamp) { Time.now.to_i } - it "marks group 0 as read" do - command = MarkGroupAsRead.new(0, Time.now.to_i, repo) - stories.should_receive(:update_all).with(is_read: true) - command.mark_group_as_read + def run_command(group_id) + MarkGroupAsRead.new(group_id, timestamp, repo) end - it "marks group 1 as read" do - command = MarkGroupAsRead.new(1, Time.now.to_i, repo) + it 'marks group as read' do + command = run_command(2) stories.should_receive(:update_all).with(is_read: true) + repo.should_receive(:fetch_unread_by_timestamp_and_group).with(timestamp, 2).and_return(stories) command.mark_group_as_read end - it "does not mark other groups as read" do - command = MarkGroupAsRead.new(2, Time.now.to_i, repo) - stories.should_not_receive(:update_all).with(is_read: true) + it 'does not mark any group as read when group is not provided' do + command = run_command(nil) + repo.should_not_receive(:fetch_unread_by_timestamp_and_group) + repo.should_not_receive(:fetch_unread_by_timestamp) command.mark_group_as_read end + + context 'SPARKS_GROUP_ID and KINDLING_GROUP_ID' do + before do + stories.should_receive(:update_all).with(is_read: true) + repo.should_receive(:fetch_unread_by_timestamp).and_return(stories) + end + + it 'marks as read all feeds when group is 0' do + command = run_command(0) + command.mark_group_as_read + end + + it 'marks as read all feeds when group is -1' do + command = run_command(-1) + command.mark_group_as_read + end + end end end diff --git a/spec/factories/feed_factory.rb b/spec/factories/feed_factory.rb index 203cf5df6..792cffab0 100644 --- a/spec/factories/feed_factory.rb +++ b/spec/factories/feed_factory.rb @@ -16,6 +16,7 @@ def as_fever_json def self.build(params = {}) FakeFeed.new( id: rand(100), + group_id: params[:group_id] || rand(100), name: params[:name] || Faker::Name.name + " on Software", url: params[:url] || Faker::Internet.url, last_fetched: params[:last_fetched] || Time.now, diff --git a/spec/factories/group_factory.rb b/spec/factories/group_factory.rb new file mode 100644 index 000000000..908b90891 --- /dev/null +++ b/spec/factories/group_factory.rb @@ -0,0 +1,16 @@ +class GroupFactory + class FakeGroup < OpenStruct + def as_fever_json + { + id: self.id, + title: self.name + } + end + end + + def self.build(params = {}) + FakeGroup.new( + id: rand(100), + name: params[:name] || Faker::Name.name + ' group') + end +end diff --git a/spec/fever_api/read_feeds_groups_spec.rb b/spec/fever_api/read_feeds_groups_spec.rb index 6cc12a12b..ee7e4e2a7 100644 --- a/spec/fever_api/read_feeds_groups_spec.rb +++ b/spec/fever_api/read_feeds_groups_spec.rb @@ -4,7 +4,7 @@ describe FeverAPI::ReadFeedsGroups do let(:feed_ids) { [5, 7, 11] } - let(:feeds) { feed_ids.map{|id| double('feed', id: id) } } + let(:feeds) { feed_ids.map{|id| double('feed', id: id, group_id: 1) } } let(:feed_repository) { double('repo') } subject do @@ -12,7 +12,8 @@ end it "returns a list of groups requested through feeds" do - feed_repository.should_receive(:list).and_return(feeds) + feed_repository.stub_chain(:in_group, :order).and_return(feeds) + subject.call('feeds' => nil).should == { feeds_groups: [ { @@ -24,7 +25,8 @@ end it "returns a list of groups requested through groups" do - feed_repository.should_receive(:list).and_return(feeds) + feed_repository.stub_chain(:in_group, :order).and_return(feeds) + subject.call('groups' => nil).should == { feeds_groups: [ { diff --git a/spec/fever_api/read_groups_spec.rb b/spec/fever_api/read_groups_spec.rb index 03542d028..59fb6cfaa 100644 --- a/spec/fever_api/read_groups_spec.rb +++ b/spec/fever_api/read_groups_spec.rb @@ -3,14 +3,23 @@ app_require "fever_api/read_groups" describe FeverAPI::ReadGroups do - subject { FeverAPI::ReadGroups.new } + let(:group_1) { double('group_1', as_fever_json: { id: 1, title: 'IT news' }) } + let(:group_2) { double('group_2', as_fever_json: { id: 2, title: 'World news' }) } + let(:group_repository) { double('repo') } - it "returns a fixed group list if requested" do + subject { FeverAPI::ReadGroups.new(group_repository: group_repository) } + + it "returns a group list if requested" do + group_repository.should_receive(:list).and_return([group_1, group_2]) subject.call('groups' => nil).should == { groups: [ { id: 1, - title: "All items" + title: "IT news" + }, + { + id: 2, + title: 'World news' } ] } diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index 33c831bfa..0fc48fb2f 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -11,7 +11,8 @@ def app let(:api_key) { 'apisecretkey' } let(:story_one) { StoryFactory.build } let(:story_two) { StoryFactory.build } - let(:feed) { FeedFactory.build } + let(:group) { GroupFactory.build } + let(:feed) { FeedFactory.build(group_id: group.id) } let(:stories) { [story_one, story_two] } let(:answer) { { api_version: 3, auth: 1, last_refreshed_on_time: Time.now.to_i } } let(:headers) { { api_key: api_key } } @@ -50,25 +51,26 @@ def make_request(extra_headers = {}) end it "returns groups and feeds by groups when 'groups' header is provided" do - FeedRepository.stub(:list).and_return([feed]) - make_request({ groups: nil }) - answer.merge!({ groups: [{ id: 1, title: "All items" }], feeds_groups: [{ group_id: 1, feed_ids: feed.id.to_s }] }) + GroupRepository.stub(:list).and_return([group]) + FeedRepository.stub_chain(:in_group, :order).and_return([feed]) + make_request(groups: nil) + answer.merge!(groups: [group.as_fever_json], feeds_groups: [{ group_id: group.id, feed_ids: feed.id.to_s }]) last_response.should be_ok last_response.body.should == answer.to_json end it "returns feeds and feeds by groups when 'feeds' header is provided" do - Feed.stub(:all).and_return([feed]) FeedRepository.stub(:list).and_return([feed]) - make_request({ feeds: nil }) - answer.merge!({ feeds: [feed.as_fever_json], feeds_groups: [{ group_id: 1, feed_ids: feed.id.to_s }] }) + FeedRepository.stub_chain(:in_group, :order).and_return([feed]) + make_request(feeds: nil) + answer.merge!(feeds: [feed.as_fever_json], feeds_groups: [{ group_id: group.id, feed_ids: feed.id.to_s }]) last_response.should be_ok last_response.body.should == answer.to_json end it "returns favicons hash when 'favicons' header provided" do - make_request({ favicons: nil }) - answer.merge!({ favicons: [{ id: 0, data: "image/gif;base64,R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" }] }) + make_request(favicons: nil) + answer.merge!(favicons: [{ id: 0, data: "image/gif;base64,R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" }]) last_response.should be_ok last_response.body.should == answer.to_json end @@ -76,47 +78,47 @@ def make_request(extra_headers = {}) it "returns stories when 'items' header is provided along with 'since_id'" do StoryRepository.should_receive(:unread_since_id).with('5').and_return([story_one]) StoryRepository.should_receive(:unread).and_return([story_one, story_two]) - make_request({ items: nil, since_id: 5 }) - answer.merge!({ items: [story_one.as_fever_json], total_items: 2 }) + make_request(items: nil, since_id: 5) + answer.merge!(items: [story_one.as_fever_json], total_items: 2) last_response.should be_ok last_response.body.should == answer.to_json end it "returns stories when 'items' header is provided without 'since_id'" do StoryRepository.should_receive(:unread).twice.and_return([story_one, story_two]) - make_request({ items: nil }) - answer.merge!({ items: [story_one.as_fever_json, story_two.as_fever_json], total_items: 2 }) + make_request(items: nil) + answer.merge!(items: [story_one.as_fever_json, story_two.as_fever_json], total_items: 2) last_response.should be_ok last_response.body.should == answer.to_json end it "returns stories ids when 'items' header is provided along with 'with_ids'" do StoryRepository.should_receive(:fetch_by_ids).twice.with(['5']).and_return([story_one]) - make_request({ items: nil, with_ids: 5 }) - answer.merge!({ items: [story_one.as_fever_json], total_items: 1 }) + make_request(items: nil, with_ids: 5) + answer.merge!(items: [story_one.as_fever_json], total_items: 1) last_response.should be_ok last_response.body.should == answer.to_json end it "returns links as empty array when 'links' header is provided" do - make_request({ links: nil }) - answer.merge!({ links: [] }) + make_request(links: nil) + answer.merge!(links: []) last_response.should be_ok last_response.body.should == answer.to_json end it "returns unread items ids when 'unread_item_ids' header is provided" do StoryRepository.should_receive(:unread).and_return([story_one, story_two]) - make_request({ unread_item_ids: nil }) - answer.merge!({ unread_item_ids: [story_one.id,story_two.id].join(',') }) + make_request(unread_item_ids: nil) + answer.merge!(unread_item_ids: [story_one.id,story_two.id].join(',')) last_response.should be_ok last_response.body.should == answer.to_json end it "returns starred items when 'saved_item_ids' header is provided" do Story.should_receive(:where).with({ is_starred: true }).and_return([story_one, story_two]) - make_request({ saved_item_ids: nil }) - answer.merge!({ saved_item_ids: [story_one.id,story_two.id].join(',') }) + make_request(saved_item_ids: nil) + answer.merge!(saved_item_ids: [story_one.id,story_two.id].join(',')) last_response.should be_ok last_response.body.should == answer.to_json end @@ -157,7 +159,14 @@ def make_request(extra_headers = {}) it "commands to mark group as read" do MarkGroupAsRead.should_receive(:new).with('10', '1375080946').and_return(double(mark_group_as_read: true)) - make_request({ mark: 'group', as: 'read', id: 10, before: 1375080946 }) + make_request(mark: 'group', as: 'read', id: 10, before: 1375080946) + last_response.should be_ok + last_response.body.should == answer.to_json + end + + it "commands to mark entire feed as read" do + MarkFeedAsRead.should_receive(:new).with('20', '1375080945').and_return(double(mark_feed_as_read: true)) + make_request(mark: 'feed', as: 'read', id: 20, before: 1375080945) last_response.should be_ok last_response.body.should == answer.to_json end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 15cc78502..a86792b2c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,6 +14,7 @@ require "factories/feed_factory" require "factories/story_factory" require "factories/user_factory" +require "factories/group_factory" require "./app" diff --git a/spec/support/files/subscriptions.xml b/spec/support/files/subscriptions.xml new file mode 100755 index 000000000..bb44ada99 --- /dev/null +++ b/spec/support/files/subscriptions.xml @@ -0,0 +1,27 @@ + + + + subscriptions title + + + + + + + + + + + + + diff --git a/spec/utils/opml_parser_spec.rb b/spec/utils/opml_parser_spec.rb index 458bb8fc3..28ed073be 100644 --- a/spec/utils/opml_parser_spec.rb +++ b/spec/utils/opml_parser_spec.rb @@ -22,12 +22,14 @@ eos - result.count.should eq 2 - result.first[:name].should eq "a sample feed" - result.first[:url].should eq "http://feeds.feedburner.com/foobar" - - result.last[:name].should eq "Matt's Blog" - result.last[:url].should eq "http://mdswanson.com/atom.xml" + resulted_values = result.values.flatten + resulted_values.size.should eq 2 + resulted_values.first[:name].should eq "a sample feed" + resulted_values.first[:url].should eq "http://feeds.feedburner.com/foobar" + + resulted_values.last[:name].should eq "Matt's Blog" + resulted_values.last[:url].should eq "http://mdswanson.com/atom.xml" + result.keys.first.should eq "Ungrouped" end it "handles nested groups of feeds" do @@ -45,10 +47,12 @@ eos + resulted_values = result.values.flatten - result.count.should eq 1 - result.first[:name].should eq "a sample feed" - result.first[:url].should eq "http://feeds.feedburner.com/foobar" + resulted_values.count.should eq 1 + resulted_values.first[:name].should eq "a sample feed" + resulted_values.first[:url].should eq "http://feeds.feedburner.com/foobar" + result.keys.first.should eq "Technology News" end it "doesn't explode when there are no feeds" do @@ -79,9 +83,10 @@ eos + resulted_values = result.values.flatten - result.count.should eq 1 - result.first[:name].should eq "a sample feed" + resulted_values.count.should eq 1 + resulted_values.first[:name].should eq "a sample feed" end end -end \ No newline at end of file +end From 48efd2ff88c5ec4219abaf9dd524a7a7d54a0e63 Mon Sep 17 00:00:00 2001 From: Matt Swanson Date: Mon, 21 Jul 2014 16:26:59 -0400 Subject: [PATCH 0035/1107] Add ability to edit feed name, closes #319 --- app/controllers/feeds_controller.rb | 2 +- app/public/css/styles.css | 2 +- app/repositories/feed_repository.rb | 3 ++- app/views/feeds/edit.erb | 8 +++++++- config/locales/en.yml | 3 ++- spec/controllers/feeds_controller_spec.rb | 4 ++-- spec/repositories/feed_repository_spec.rb | 7 ++++--- 7 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/controllers/feeds_controller.rb b/app/controllers/feeds_controller.rb index 8670d0a95..1b84c0806 100644 --- a/app/controllers/feeds_controller.rb +++ b/app/controllers/feeds_controller.rb @@ -18,7 +18,7 @@ class Stringer < Sinatra::Base put "/feeds/:id" do feed = FeedRepository.fetch(params[:id]) - FeedRepository.update_url(feed, params[:feed_url]) + FeedRepository.update_feed(feed, params[:feed_name], params[:feed_url]) flash[:success] = t('feeds.edit.flash.updated_successfully') redirect to('/feeds') diff --git a/app/public/css/styles.css b/app/public/css/styles.css index f4e97da8a..90750b93e 100644 --- a/app/public/css/styles.css +++ b/app/public/css/styles.css @@ -477,7 +477,7 @@ li.feed .remove-feed a:hover { transition: 0.25s; } -.setup #password, .setup #password-confirmation, .setup #feed-url { +.setup #password, .setup #password-confirmation, .setup #feed-url, .setup #feed-name { padding-left: 100px; padding-right: 36px; } diff --git a/app/repositories/feed_repository.rb b/app/repositories/feed_repository.rb index ad698448b..37fa9a822 100644 --- a/app/repositories/feed_repository.rb +++ b/app/repositories/feed_repository.rb @@ -11,7 +11,8 @@ def self.fetch_by_ids(ids) Feed.where(id: ids) end - def self.update_url(feed, url) + def self.update_feed(feed, name, url) + feed.name = name feed.url = url feed.save end diff --git a/app/views/feeds/edit.erb b/app/views/feeds/edit.erb index 2bc7674e7..7209fe014 100644 --- a/app/views/feeds/edit.erb +++ b/app/views/feeds/edit.erb @@ -9,10 +9,16 @@
    - + + + +
    +
    +
    +
    diff --git a/config/locales/en.yml b/config/locales/en.yml index b430c17b0..a15c7bfd1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -36,9 +36,10 @@ en: edit: fields: feed_url: Feed URL + feed_name: Feed Name submit: Save flash: - updated_successfully: Updated the feed URL for ya'! + updated_successfully: Updated the feed for ya'! first_run: password: anti_social: anti-social diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index c1f7ab2b1..92f204b5b 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -42,9 +42,9 @@ it "updates a feed given the id" do feed = FeedFactory.build(url: 'example.com/atom') FeedRepository.should_receive(:fetch).with("123").and_return(feed) - FeedRepository.should_receive(:update_url).with(feed, 'example.com/feed') + FeedRepository.should_receive(:update_feed).with(feed, 'Test', 'example.com/feed') - put "/feeds/123", feed_id: "123", feed_url: "example.com/feed" + put "/feeds/123", feed_id: "123", feed_name: "Test", feed_url: "example.com/feed" last_response.should be_redirect end diff --git a/spec/repositories/feed_repository_spec.rb b/spec/repositories/feed_repository_spec.rb index b0ce62d1a..9111dbd9b 100644 --- a/spec/repositories/feed_repository_spec.rb +++ b/spec/repositories/feed_repository_spec.rb @@ -43,12 +43,13 @@ end end - describe ".update_url" do - it "saves the url" do + describe ".update_feed" do + it "saves the name and url" do feed = Feed.new - FeedRepository.update_url(feed, 'example.com/feed') + FeedRepository.update_feed(feed, 'Test Feed', 'example.com/feed') + feed.name.should eq 'Test Feed' feed.url.should eq 'example.com/feed' end end From edaf8529e583a64219d37e4f523424c2a5f8ad25 Mon Sep 17 00:00:00 2001 From: Matt Swanson Date: Mon, 21 Jul 2014 16:31:20 -0400 Subject: [PATCH 0036/1107] Update translations --- config/locales/de.yml | 8 ++++++++ config/locales/el-GR.yml | 8 ++++++++ config/locales/en.yml | 8 ++++---- config/locales/es.yml | 8 ++++++++ config/locales/fr.yml | 8 ++++++++ config/locales/he.yml | 8 ++++++++ config/locales/it.yml | 8 ++++++++ config/locales/ja.yml | 8 ++++++++ config/locales/nl.yml | 8 ++++++++ config/locales/pt-BR.yml | 8 ++++++++ config/locales/pt.yml | 8 ++++++++ config/locales/ru.yml | 8 ++++++++ config/locales/sv.yml | 8 ++++++++ config/locales/tr.yml | 8 ++++++++ config/locales/zh-CN.yml | 8 ++++++++ 15 files changed, 116 insertions(+), 4 deletions(-) diff --git a/config/locales/de.yml b/config/locales/de.yml index 52f874161..d034b7bbd 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -30,6 +30,13 @@ de: already_subscribed_error: Du hast diesen Feed bereits abonniert... feed_not_found_error: Wir konnten diesen Feed nicht finden. Probiere es noch einmal title: Benötigst du neue Geschichten? + edit: + fields: + feed_name: + feed_url: + submit: + flash: + updated_successfully: index: add: hinzufügen add_some_feeds: Hey, du solltest ein paar Feeds %{add}. @@ -89,6 +96,7 @@ de: shortcuts: keys: a: Einen Feed hinzufügen + f: jk: Nächste/vorherige Geschichte left: Vorige Seite m: Markiere Geschichte als gelesen/ungelesen diff --git a/config/locales/el-GR.yml b/config/locales/el-GR.yml index 67ba40c13..6fe0bda98 100644 --- a/config/locales/el-GR.yml +++ b/config/locales/el-GR.yml @@ -30,6 +30,13 @@ el-GR: already_subscribed_error: Είστε ήδη εγγεγραμενος σ' αυτο το ιστολόγιο... feed_not_found_error: Δεν μπορέσαμε να βρούμε αυτο το ιστολόγιο. Προσπαθήστε ξανά. title: Όρεξη για καινούριες ειδήσεις? + edit: + fields: + feed_name: + feed_url: + submit: + flash: + updated_successfully: index: add: εισάγεις add_some_feeds: Επ! Γιατί δεν %{add} μερικά ιστολόγια στη συλλογή σου? @@ -89,6 +96,7 @@ el-GR: shortcuts: keys: a: + f: jk: Επόμενη/Προηγούμενη είδηση left: m: Σημειώστε το νέο ως αναγνωσμένο ή όχι diff --git a/config/locales/en.yml b/config/locales/en.yml index a15c7bfd1..5ac0b0441 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -30,16 +30,16 @@ en: already_subscribed_error: You are already subscribed to this feed... feed_not_found_error: We couldn't find that feed. Try again. title: Need new stories? - index: - add: add - add_some_feeds: Hey, you should %{add} some feeds. edit: fields: - feed_url: Feed URL feed_name: Feed Name + feed_url: Feed URL submit: Save flash: updated_successfully: Updated the feed for ya'! + index: + add: add + add_some_feeds: Hey, you should %{add} some feeds. first_run: password: anti_social: anti-social diff --git a/config/locales/es.yml b/config/locales/es.yml index cb2ab1a9c..e9920bfd4 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -30,6 +30,13 @@ es: already_subscribed_error: Ya te suscribiste a esta feed... feed_not_found_error: No pudimos encontrar esa feed. Inténtalo de vuelta. title: ¿Necesitas nuevas historias? + edit: + fields: + feed_name: + feed_url: + submit: + flash: + updated_successfully: index: add: agregar add_some_feeds: Oye, deberias %{add} algunas feeds. @@ -89,6 +96,7 @@ es: shortcuts: keys: a: Añadir una feed + f: jk: Siguiente/previa historia left: Página anterior m: Marcar item como leído/no leído diff --git a/config/locales/fr.yml b/config/locales/fr.yml index bb6fde9a6..fe2b4e69c 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -30,6 +30,13 @@ fr: already_subscribed_error: Vous suivez déjà ce flux... feed_not_found_error: Nous n'avons pas pu trouver ce flux. Essayez de nouveau. title: Besoin de nouveaux articles ? + edit: + fields: + feed_name: + feed_url: + submit: + flash: + updated_successfully: index: add: ajouter add_some_feeds: Vous devriez %{add} quelques flux. @@ -89,6 +96,7 @@ fr: shortcuts: keys: a: Ajouter un flux + f: jk: Article suivant/précédent left: Page précédente m: Marquer ceci comme lu/non lu diff --git a/config/locales/he.yml b/config/locales/he.yml index 20ac0e246..3e72a5abc 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -30,6 +30,13 @@ he: already_subscribed_error: הפיד הנ"ל כבר נמצא במעקב. feed_not_found_error: לא הצלחנו למצוא את הפיד. נסה שוב. title: מחפש מה לקרוא? + edit: + fields: + feed_name: + feed_url: + submit: + flash: + updated_successfully: index: add: להוסיף add_some_feeds: הי, כדאי %{add} קצת פידים. @@ -89,6 +96,7 @@ he: shortcuts: keys: a: + f: jk: סיפור הבא/קודם left: m: סמן את הכל כנקרא/לא נקרא diff --git a/config/locales/it.yml b/config/locales/it.yml index 8424e2c23..19c645af0 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -30,6 +30,13 @@ it: already_subscribed_error: Sei già sottoscritto a questo feed... feed_not_found_error: Non siamo riusciti a trovare il feed. Riprova. title: Bisogno di nuove storie? + edit: + fields: + feed_name: + feed_url: + submit: + flash: + updated_successfully: index: add: aggiungere add_some_feeds: Hey, dovresti %{add} qualche feed. @@ -89,6 +96,7 @@ it: shortcuts: keys: a: Aggiungi un feed + f: jk: Prossima/precedente storia left: Pagina precedente m: Segna come letto/non letto diff --git a/config/locales/ja.yml b/config/locales/ja.yml index b17f7ab83..45b8458aa 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -30,6 +30,13 @@ ja: already_subscribed_error: このフィードは既に登録されてます feed_not_found_error: フィードを見つけられませんでした、もう一度試して下さい title: 新しいストーリーが必要ですか? + edit: + fields: + feed_name: + feed_url: + submit: + flash: + updated_successfully: index: add: 追加 add_some_feeds: 何かフィードを%{add}する必要があります @@ -89,6 +96,7 @@ ja: shortcuts: keys: a: フィードを追加 + f: jk: 次/前のストーリー left: 前ページ m: 既読/未読切り替え diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 1e546d975..4b9a95f8a 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -30,6 +30,13 @@ nl: already_subscribed_error: Je bent al geabonneerd op deze feed... feed_not_found_error: Die feed konden we niet vinden. Probeer het opnieuw. title: Nieuwe artikelen nodig? + edit: + fields: + feed_name: + feed_url: + submit: + flash: + updated_successfully: index: add: toevoegen add_some_feeds: Hé, je zou eens wat feeds kunnen %{add}. @@ -89,6 +96,7 @@ nl: shortcuts: keys: a: Een feed toevoegen + f: jk: Volgend/vorig artikel left: Vorige pagina m: Artikel markeren als gelezen/ongelezen diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index bb196f48e..7c22b1a36 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -30,6 +30,13 @@ pt-BR: already_subscribed_error: Você já está inscrito neste feed... feed_not_found_error: Não conseguimos achar este feed. Tente novamente. title: Precisa de novas histórias? + edit: + fields: + feed_name: + feed_url: + submit: + flash: + updated_successfully: index: add: adicionar add_some_feeds: Ei, você deveria %{add} alguns feeds. @@ -89,6 +96,7 @@ pt-BR: shortcuts: keys: a: Adicione um feed + f: jk: História próxima/anterior left: Página anterior m: Marcar item como lido/não lido diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 0808368c0..bbae5e47f 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -30,6 +30,13 @@ pt: already_subscribed_error: Você já subscreveu esta feed... feed_not_found_error: Não foi possível encontrar a feed. Tente novamente. title: Precisa de novas histórias? + edit: + fields: + feed_name: + feed_url: + submit: + flash: + updated_successfully: index: add: adicionar add_some_feeds: Ei, você deveria %{add} algumas feeds. @@ -89,6 +96,7 @@ pt: shortcuts: keys: a: + f: jk: Próxima história/História anterior left: m: Marcar item como lido/não lido diff --git a/config/locales/ru.yml b/config/locales/ru.yml index ab8a6ce29..217f10445 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -30,6 +30,13 @@ ru: already_subscribed_error: Вы уже подписаны на этот фид... feed_not_found_error: Мы не смогли найти этот фид. Попробуйте еще раз. title: Нужны новые истории? + edit: + fields: + feed_name: + feed_url: + submit: + flash: + updated_successfully: index: add: добавить add_some_feeds: Эй, ​​Вы должны %{add} некоторые фид каналы. @@ -89,6 +96,7 @@ ru: shortcuts: keys: a: + f: jk: Перейти на следующую/предыдущую историю left: Предыдущая страница m: Пометить как прочитанное/непрочитанное diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 5a0a24137..60d2b080a 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -30,6 +30,13 @@ sv: already_subscribed_error: Du prenumererar redan på den här feeden... feed_not_found_error: Vi kunde inte hitta feeden. Prova igen. title: Behöver du nya berättelser? + edit: + fields: + feed_name: + feed_url: + submit: + flash: + updated_successfully: index: add: lägga till add_some_feeds: Hej, du borde %{add} några feeds. @@ -89,6 +96,7 @@ sv: shortcuts: keys: a: Lägg till en feed + f: jk: Nästa/föregående berättelse left: Föregående sida m: Markera som läst/oläst diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 51c360412..06bf88d34 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -30,6 +30,13 @@ tr: already_subscribed_error: Bu beslemeye zaten kayitlisiniz... feed_not_found_error: Bu beslemeyi bulamadik. Tekrar deneyiniz. title: Yeni hikayelere mi ihtiyaciniz var? + edit: + fields: + feed_name: + feed_url: + submit: + flash: + updated_successfully: index: add: ekle add_some_feeds: Hey, you should %{add} some feeds. @@ -89,6 +96,7 @@ tr: shortcuts: keys: a: + f: jk: Sonraki/onceki hikaye left: m: Okundu/okunmadi olarak isaretle diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index b5617bb38..c25c826ba 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -30,6 +30,13 @@ zh-CN: already_subscribed_error: 你已经订阅过这个供稿了哟... feed_not_found_error: 呃,我们无法识别这个供稿地址。麻烦你检查后再试一次。 title: 想要添加新内容? + edit: + fields: + feed_name: + feed_url: + submit: + flash: + updated_successfully: index: add: 添加 add_some_feeds: 你应该%{add}一些订阅哟~ @@ -89,6 +96,7 @@ zh-CN: shortcuts: keys: a: 添加新订阅 + f: jk: 下一个/上一个故事 left: 上一页 m: 将一个条目标为已读/未读 From 697ad7e31e159a8721c259170cb8e1b1dea55021 Mon Sep 17 00:00:00 2001 From: Matt Swanson Date: Mon, 21 Jul 2014 16:50:01 -0400 Subject: [PATCH 0037/1107] Fix code tags overflowing container, closes #315 --- app/public/css/styles.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/public/css/styles.css b/app/public/css/styles.css index 90750b93e..c2e4df1bf 100644 --- a/app/public/css/styles.css +++ b/app/public/css/styles.css @@ -15,6 +15,10 @@ hr { margin: 20px auto; } +code { + white-space: normal; +} + .container { width: 100%; max-width: 720px; From 5b17ddcc2eafd4f3d83f8bcb4ceda4e41d050b94 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Sun, 27 Jul 2014 12:52:51 +0200 Subject: [PATCH 0038/1107] Style stories marked as "keep unread" Adds styling to stories marked as "keep unread", so they can be distinguished from never-read stories. Fixes #210. --- app/public/css/styles.css | 4 ++++ app/public/js/app.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/public/css/styles.css b/app/public/css/styles.css index c2e4df1bf..53d5f716a 100644 --- a/app/public/css/styles.css +++ b/app/public/css/styles.css @@ -136,6 +136,10 @@ li.story.read { opacity: 0.5; } +li.story.keepUnread .story-preview { + font-weight: bold; +} + li.story.open { opacity: 1.0; } diff --git a/app/public/js/app.js b/app/public/js/app.js index 75cc47d85..1ca989b85 100644 --- a/app/public/js/app.js +++ b/app/public/js/app.js @@ -105,6 +105,9 @@ var StoryView = Backbone.View.extend({ if (jsonModel.is_read) { this.$el.addClass('read'); } + if (jsonModel.keep_unread) { + this.$el.addClass('keepUnread'); + } return this; }, @@ -131,6 +134,7 @@ var StoryView = Backbone.View.extend({ itemKeepUnread: function() { var icon = this.model.get("keep_unread") ? "icon-check" : "icon-check-empty"; this.$(".story-keep-unread > i").attr("class", icon); + this.$el.toggleClass("keepUnread", this.model.get("keep_unread")); }, itemStarred: function() { From 5cf9cbb41c42db0bcbfd0f17cc26855fa3757888 Mon Sep 17 00:00:00 2001 From: Matt Swanson Date: Mon, 4 Aug 2014 14:09:54 -0400 Subject: [PATCH 0039/1107] Enable gzip compression when parsing feeds, closes #324 --- app/tasks/fetch_feed.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/tasks/fetch_feed.rb b/app/tasks/fetch_feed.rb index 6c1064e34..dbd21a9b8 100644 --- a/app/tasks/fetch_feed.rb +++ b/app/tasks/fetch_feed.rb @@ -16,7 +16,15 @@ def initialize(feed, feed_parser = Feedjira::Feed, logger = nil) def fetch begin - raw_feed = @parser.fetch_and_parse(@feed.url, user_agent: USER_AGENT, if_modified_since: @feed.last_fetched, timeout: 30, max_redirects: 2) + options = { + user_agent: USER_AGENT, + if_modified_since: @feed.last_fetched, + timeout: 30, + max_redirects: 2, + compress: true + } + + raw_feed = @parser.fetch_and_parse(@feed.url, options) if raw_feed == 304 @logger.info "#{@feed.url} has not been modified since last fetch" if @logger From de5c91d005557576fb29eb3bb548351a76cb2e70 Mon Sep 17 00:00:00 2001 From: Matt Swanson Date: Thu, 7 Aug 2014 14:06:52 -0400 Subject: [PATCH 0040/1107] Add support for Heroku button; move docs around a bit --- Gemfile | 7 -- Gemfile.lock | 8 -- README.md | 40 +------- Rakefile | 133 +------------------------- app.json | 27 ++++++ app/controllers/debug_controller.rb | 4 + app/helpers/authentication_helpers.rb | 1 + app/views/heroku.erb | 19 ++++ config/locales/en.yml | 3 + docs/Heroku.md | 36 +++++++ screenshots/logo.png | Bin 0 -> 22108 bytes 11 files changed, 96 insertions(+), 182 deletions(-) create mode 100644 app.json create mode 100644 app/views/heroku.erb create mode 100644 docs/Heroku.md create mode 100644 screenshots/logo.png diff --git a/Gemfile b/Gemfile index 56c228de9..6d535f8fd 100644 --- a/Gemfile +++ b/Gemfile @@ -21,13 +21,6 @@ group :development, :test do gem "shotgun", "~> 0.9.0" end -group :heroku do - gem "excon", "~> 0.31.0" - gem "formatador", "~> 0.2.4" - gem "netrc", "~> 0.7.7" - gem "rendezvous", "~> 0.0.2" -end - gem "activerecord", "~> 4.0" # need to work around bug in 4.0.1 https://github.com/rails/arel/pull/216 gem 'arel', git: 'git://github.com/rails/arel.git', branch: '4-0-stable' diff --git a/Gemfile.lock b/Gemfile.lock index e45c2a291..87ad7486f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -48,7 +48,6 @@ GEM diff-lcs (1.2.5) docile (1.1.1) dotenv (0.9.0) - excon (0.31.0) faker (1.2.0) i18n (~> 0.5) feedbag (0.9.2) @@ -60,7 +59,6 @@ GEM foreman (0.63.0) dotenv (>= 0.7) thor (>= 0.13.6) - formatador (0.2.4) highline (1.6.20) hpricot (0.8.6) i18n (0.6.9) @@ -73,7 +71,6 @@ GEM mini_portile (0.5.2) minitest (4.7.5) multi_json (1.8.2) - netrc (0.7.7) nokogiri (1.6.1) mini_portile (~> 0.5.0) pg (0.17.1) @@ -94,7 +91,6 @@ GEM rack-test (>= 0.5) raindrops (0.12.0) rake (10.1.1) - rendezvous (0.0.2) rest-client (1.6.7) mime-types (>= 1.16) rspec (2.14.1) @@ -165,23 +161,19 @@ DEPENDENCIES coveralls (~> 0.7) delayed_job (~> 4.0) delayed_job_active_record (~> 4.0) - excon (~> 0.31.0) faker (~> 1.2) feedbag (~> 0.9.2) feedjira (~> 1.3.0) foreman (~> 0.63.0) - formatador (~> 0.2.4) highline (~> 1.6, >= 1.6.20) i18n (~> 0.6.9) loofah (~> 2.0.0) - netrc (~> 0.7.7) nokogiri (~> 1.6) pg (~> 0.17.1) pry-byebug (~> 1.2) rack-test (~> 0.6.2) racksh (~> 1.0) rake (~> 10.1, >= 10.1.1) - rendezvous (~> 0.0.2) rspec (~> 2.14, >= 2.14.1) rspec-html-matchers (~> 0.4.3) shotgun (~> 0.9.0) diff --git a/README.md b/README.md index e2e5358b8..e7f4fd723 100644 --- a/README.md +++ b/README.md @@ -18,42 +18,12 @@ But it does have keyboard shortcuts and was made with love! Stringer is a Ruby (2.0.0+) app based on Sinatra, ActiveRecord, PostgreSQL, Backbone.js and DelayedJob. -Instructions are provided for deploying to Heroku (runs fine on the free plan) but Stringer can be deployed anywhere that supports Ruby (setup instructions for a Linux-based VPS are provided [here](/docs/VPS.md), and for OpenShift, provided [here](/docs/OpenShift.md)). +[![Deploy to Heroku](https://cdn.herokuapp.com/deploy/button.svg)](https://heroku.com/deploy) -```sh -git clone git://github.com/swanson/stringer.git -cd stringer -heroku create -git push heroku master - -heroku config:set APP_URL=`heroku apps:info | grep -o 'http[^"]*'` -heroku config:set SECRET_TOKEN=`openssl rand -hex 20` - -heroku run rake db:migrate -heroku restart - -heroku addons:add scheduler -heroku addons:open scheduler -``` - -Add an hourly task that runs `rake lazy_fetch` (if you are not on Heroku you may want `rake fetch_feeds` instead). - -Load the app and follow the instructions to import your feeds and start using the app. +Stringer will run just fine on the Heroku free plan. ---- - -In the event that you need to change your password, run `heroku run rake change_password` from the app folder. - -## Updating the app - -From the app's directory: - -```sh -git pull -git push heroku master -heroku run rake db:migrate -heroku restart -``` +Instructions are provided for deploying to [Heroku manually](/docs/Heroku.md), to any Ruby +compatible [Linux-based VPS](/docs/VPS.md), and to [OpenShift](/docs/OpenShift.md). ## Niceties @@ -107,7 +77,7 @@ To set your locale on Heroku, run `heroku config:set LOCALE=en`. If you would like to translate Stringer to your preferred language, please use [LocaleApp](http://www.localeapp.com/projects/4637). -### Clean up old read stories +### Clean up old read stories on Heroku If you are on the Heroku free plan, there is a 10k row limit so you will eventually run out of space. diff --git a/Rakefile b/Rakefile index d2983e70d..0a0193574 100644 --- a/Rakefile +++ b/Rakefile @@ -81,135 +81,4 @@ begin task :default => [:speedy_tests] rescue LoadError # allow for bundle install --without development:test -end - -desc "deploy stringer on Heroku" -task :deploy do - - require 'excon' - require 'formatador' - require 'json' - require 'netrc' - require 'rendezvous' - require 'securerandom' - - Formatador.display_line("[negative]<> deploying stringer to Heroku[/]") - - # grab netrc credentials, set by toolbelt via `heroku login` - Formatador.display_line("[negative]<> reading your global Heroku credentials from ~/.netrc (set when you ran heroku login)...[/]") - _, password = Netrc.read['api.heroku.com'] - - # setup excon for API calls - heroku = Excon.new( - 'https://api.heroku.com', - :headers => { - "Accept" => "application/vnd.heroku+json; version=3", - "Authorization" => "Basic #{[':' << password].pack('m').delete("\r\n")}", - "Content-Type" => "application/json" - } - ) - - #heroku create - Formatador.display_line("[negative]<> creating app[/]") - app_data = JSON.parse(heroku.post(:path => "/apps").body) - - #git push heroku master - Formatador.display_line("[negative]<> pushing code to [underline]#{app_data['name']}[/]") - `git push git@heroku.com:#{app_data['name']}.git master` - - heroku.reset # reset socket as git push may take long enough for timeout - - #heroku config:set SECRET_TOKEN=`openssl rand -hex 20` - Formatador.display_line("[negative]<> setting SECRET_TOKEN on [underline]#{app_data['name']}[/]") - heroku.patch( - :body => { "SECRET_TOKEN" => SecureRandom.hex(20) }.to_json, - :path => "/apps/#{app_data['id']}/config-vars" - ) - - #heroku run rake db:migrate - Formatador.display_line("[negative]<> running `rake db:migrate` on [underline]#{app_data['name']}[/]") - run_data = JSON.parse(heroku.post( - :body => { - "attach" => true, - "command" => "rake db:migrate" - }.to_json, - :path => "/apps/#{app_data['id']}/dynos" - ).body) - Rendezvous.start( - :url => run_data['attach_url'] - ) - - heroku.reset # reset socket as db:migrate may take long enough for timeout - - #heroku restart - Formatador.display_line("[negative]<> restarting [underline]#{app_data['name']}[/]") - heroku.delete(:path => "/apps/#{app_data['id']}/dynos") - - #heroku addons:add scheduler - Formatador.display_line("[negative]<> adding scheduler:standard to [underline]#{app_data['name']}[/]") - heroku.post( - :body => { "plan" => { "name" => "scheduler:standard" } }.to_json, - :path => "/apps/#{app_data['id']}/addons" - ) - - #heroku addons:open scheduler - Formatador.display_lines([ - "[negative]<> Add `[bold]rake lazy_fetch[/][negative]` hourly task at [underline]https://api.heroku.com/apps/#{app_data['id']}/addons/scheduler:standard[/]", - "[negative]<> Impatient? After adding feeds, immediately fetch the latest with `heroku run rake fetch_feeds -a #{app_data['name']}`", - "[negative]<> stringer available at [underline]#{app_data['web_url']}[/]" - ]) -end - -desc "update stringer on heroku" -task :update, :app do |task, args| - - require 'excon' - require 'formatador' - require 'json' - require 'netrc' - require 'rendezvous' - - unless args.app - Formatador.display_line("[negative]! Error: App required, please run as `bundle exec rake update[app]`[/]") - exit - end - - Formatador.display_line("[negative]<> updating Heroku stringer on [underline]#{args.app}[/]") - - # grab netrc credentials, set by toolbelt via `heroku login` - Formatador.display_line("[negative]<> reading your global Heroku credentials from ~/.netrc (set when you ran heroku login)...[/]") - _, password = Netrc.read['api.heroku.com'] - - # setup excon for API calls - heroku = Excon.new( - 'https://api.heroku.com', - :headers => { - "Accept" => "application/vnd.heroku+json; version=3", - "Authorization" => "Basic #{[':' << password].pack('m').delete("\r\n")}", - "Content-Type" => "application/json" - } - ) - - #git push heroku master - Formatador.display_line("[negative]<> pushing code to [underline]#{args.app}[/]") - `git push git@heroku.com:#{args.app}.git master` - - #heroku run rake db:migrate - Formatador.display_line("[negative]<> running `rake db:migrate` on [underline]#{args.app}[/]") - run_data = JSON.parse(heroku.post( - :body => { - "attach" => true, - "command" => "rake db:migrate" - }.to_json, - :path => "/apps/#{args.app}/dynos" - ).body) - Rendezvous.start( - :url => run_data['attach_url'] - ) - - heroku.reset # reset socket as db:migrate may take long enough for timeout - - #heroku restart - Formatador.display_line("[negative]<> restarting [underline]#{args.app}[/]") - heroku.delete(:path => "/apps/#{args.app}/dynos") -end +end \ No newline at end of file diff --git a/app.json b/app.json new file mode 100644 index 000000000..030eea0ab --- /dev/null +++ b/app.json @@ -0,0 +1,27 @@ +{ + "name": "Stringer", + "description": "A self-hosted, anti-social RSS reader.", + "logo": "https://raw.githubusercontent.com/swanson/testing-hb/master/screenshots/logo.png", + "keywords": [ + "RSS", + "Ruby" + ], + "website": "https://github.com/swanson/stringer", + "success_url": "/heroku", + "scripts": { + "postdeploy": "rake db:migrate" + }, + "env": { + "SECRET_TOKEN": { + "description": "Secret key used as the session secret", + "generator": "secret" + }, + "LOCALE": { + "description": "Specify the translation locale you wish to use", + "value": "en" + } + }, + "addons": [ + "scheduler:standard" + ] +} \ No newline at end of file diff --git a/app/controllers/debug_controller.rb b/app/controllers/debug_controller.rb index fec72216e..5bb739a07 100644 --- a/app/controllers/debug_controller.rb +++ b/app/controllers/debug_controller.rb @@ -7,4 +7,8 @@ class Stringer < Sinatra::Base pending_migrations: MigrationStatus.new.pending_migrations } end + + get "/heroku" do + erb :heroku + end end diff --git a/app/helpers/authentication_helpers.rb b/app/helpers/authentication_helpers.rb index ae4b20b72..ca5661159 100644 --- a/app/helpers/authentication_helpers.rb +++ b/app/helpers/authentication_helpers.rb @@ -13,6 +13,7 @@ def needs_authentication?(path) return false if !UserRepository.setup_complete? return false if path == "/login" || path == "/logout" return false if path =~ /css/ || path =~ /js/ || path =~ /img/ + return false if path == "/heroku" true end diff --git a/app/views/heroku.erb b/app/views/heroku.erb new file mode 100644 index 000000000..07a13bacd --- /dev/null +++ b/app/views/heroku.erb @@ -0,0 +1,19 @@ +
    +

    <%= t('tutorial.heroku_one_more_thing') %>

    + +

    + <%= t('tutorial.heroku_hourly_task') %> +

    +

    + <%= t('tutorial.heroku_scheduler') %>: +

    +
    +    Task: rake lazy_fetch
    +    Dyno Size: 1X
    +    Frequency: Hourly
    +  
    +
    + + \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 5ac0b0441..7d00924b8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -142,6 +142,9 @@ en: as_read: as read click_to_read: (click to read) description: We're getting you some stories to read, give us a second. + heroku_hourly_task: You need to add an hourly task to check for new stories. + heroku_one_more_thing: One more thing... + heroku_scheduler: Go to the Heroku Scheduler and add this task mark_all: mark all ready: Okay, it's ready! refresh: refresh diff --git a/docs/Heroku.md b/docs/Heroku.md new file mode 100644 index 000000000..f14600b10 --- /dev/null +++ b/docs/Heroku.md @@ -0,0 +1,36 @@ +```sh +git clone git://github.com/swanson/stringer.git +cd stringer +heroku create +git push heroku master + +heroku config:set APP_URL=`heroku apps:info | grep -o 'http[^"]*'` +heroku config:set SECRET_TOKEN=`openssl rand -hex 20` + +heroku run rake db:migrate +heroku restart + +heroku addons:add scheduler +heroku addons:open scheduler +``` + +Add an hourly task that runs `rake lazy_fetch` (if you are not on Heroku you may want `rake fetch_feeds` instead). + +Load the app and follow the instructions to import your feeds and start using the app. + +See the "Niceties" section of the README for a few more tips and tricks for getting the most out of Stringer on Heroku. + +## Updating the app + +From the app's directory: + +```sh +git pull +git push heroku master +heroku run rake db:migrate +heroku restart +``` + +## Password Reset + +In the event that you need to change your password, run `heroku run rake change_password` from the app folder. \ No newline at end of file diff --git a/screenshots/logo.png b/screenshots/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8f894864345a1ff2e7923bb0751767c6fcf8d756 GIT binary patch literal 22108 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 Date: Thu, 7 Aug 2014 15:25:07 -0400 Subject: [PATCH 0041/1107] Add new translations --- config/locales/de.yml | 3 +++ config/locales/el-GR.yml | 3 +++ config/locales/en.yml | 2 +- config/locales/es.yml | 3 +++ config/locales/fr.yml | 3 +++ config/locales/he.yml | 3 +++ config/locales/it.yml | 3 +++ config/locales/ja.yml | 3 +++ config/locales/nl.yml | 3 +++ config/locales/pt-BR.yml | 3 +++ config/locales/pt.yml | 3 +++ config/locales/ru.yml | 3 +++ config/locales/sv.yml | 3 +++ config/locales/tr.yml | 3 +++ config/locales/zh-CN.yml | 3 +++ 15 files changed, 43 insertions(+), 1 deletion(-) diff --git a/config/locales/de.yml b/config/locales/de.yml index d034b7bbd..882a612b3 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -142,6 +142,9 @@ de: as_read: als gelesen markieren click_to_read: (klicken um zu lesen) description: Wir besorgen dir Geschichten zum Lesen, gib uns eine Sekunde. + heroku_hourly_task: + heroku_one_more_thing: + heroku_scheduler: mark_all: alle ready: Okay, es ist bereit! refresh: aktualisieren diff --git a/config/locales/el-GR.yml b/config/locales/el-GR.yml index 6fe0bda98..ba7cad7b5 100644 --- a/config/locales/el-GR.yml +++ b/config/locales/el-GR.yml @@ -142,6 +142,9 @@ el-GR: as_read: ως αναγνωσμένα click_to_read: (πάτα εδώ για ανάγνωση) description: Μια στιγμή, ετοιμάζουμε τις ειδήσεις προς ανάγνωση. + heroku_hourly_task: + heroku_one_more_thing: + heroku_scheduler: mark_all: επισήμανση όλων ready: Έτοιμοι! refresh: ανανέωση diff --git a/config/locales/en.yml b/config/locales/en.yml index 7d00924b8..ec62a3f53 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -142,7 +142,7 @@ en: as_read: as read click_to_read: (click to read) description: We're getting you some stories to read, give us a second. - heroku_hourly_task: You need to add an hourly task to check for new stories. + heroku_hourly_task: You need to add an hourly task to check for new stories. heroku_one_more_thing: One more thing... heroku_scheduler: Go to the Heroku Scheduler and add this task mark_all: mark all diff --git a/config/locales/es.yml b/config/locales/es.yml index e9920bfd4..93854e54a 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -142,6 +142,9 @@ es: as_read: como leído click_to_read: (haz click para leer) description: Estamos consiguiendo unas historias para leer, danos un moment. + heroku_hourly_task: + heroku_one_more_thing: + heroku_scheduler: mark_all: Marcar todas ready: ¡Bueno esta listo! refresh: refrescar diff --git a/config/locales/fr.yml b/config/locales/fr.yml index fe2b4e69c..83c32960c 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -142,6 +142,9 @@ fr: as_read: comme lu click_to_read: (cliquer pour lire) description: Nous récupérons quelques articles à lire, donnez-nous une seconde. + heroku_hourly_task: + heroku_one_more_thing: + heroku_scheduler: mark_all: tout marquer ready: Ok, c'est prêt ! refresh: rafraîchir diff --git a/config/locales/he.yml b/config/locales/he.yml index 3e72a5abc..7cc1cff2f 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -142,6 +142,9 @@ he: as_read: כנקרא click_to_read: (לקריאה) description: אנחנו מחפשים סיפורים בשבילך, תן לנו קצת זמן. + heroku_hourly_task: + heroku_one_more_thing: + heroku_scheduler: mark_all: סמן הכל ready: יפה, הכל מוכן! refresh: רענון diff --git a/config/locales/it.yml b/config/locales/it.yml index 19c645af0..db58281b7 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -142,6 +142,9 @@ it: as_read: come lette click_to_read: (clicca per leggere) description: Stiamo importando alcune storie da leggere, dacci un secondo. + heroku_hourly_task: + heroku_one_more_thing: + heroku_scheduler: mark_all: segna tutte ready: Okay, è pronto! refresh: aggiorna diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 45b8458aa..3ec0beade 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -142,6 +142,9 @@ ja: as_read: as read click_to_read: (click to read) description: あなたのストーリーを読み込んでます、しばらくお待ち下さい + heroku_hourly_task: + heroku_one_more_thing: + heroku_scheduler: mark_all: mark all ready: 準備OK! refresh: refresh diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 4b9a95f8a..78de1693c 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -142,6 +142,9 @@ nl: as_read: als gelezen markeren click_to_read: (klik om te lezen) description: We zijn je artikelen aan het ophalen, geef ons even. + heroku_hourly_task: + heroku_one_more_thing: + heroku_scheduler: mark_all: alles ready: Okay, klaar! refresh: vernieuwen diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 7c22b1a36..f0cadc159 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -142,6 +142,9 @@ pt-BR: as_read: como lido click_to_read: (clique para ler) description: Nós estamos pegando algumas histórias para leitura, dê-nos um segundo. + heroku_hourly_task: + heroku_one_more_thing: + heroku_scheduler: mark_all: marcar todos ready: Ok, está pronto! refresh: atualizar diff --git a/config/locales/pt.yml b/config/locales/pt.yml index bbae5e47f..a91f6bd69 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -142,6 +142,9 @@ pt: as_read: como lido click_to_read: (clique para ler) description: Estamos a actualizar as suas histórias, dê-nos um segundo. + heroku_hourly_task: + heroku_one_more_thing: + heroku_scheduler: mark_all: marcar todos ready: Ok, está pronto! refresh: atualizar diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 217f10445..9ecd00fe8 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -142,6 +142,9 @@ ru: as_read: как прочитанное click_to_read: (нажмите, чтобы прочитать) description: Мы получаем ваши истории для чтения, секунду. + heroku_hourly_task: + heroku_one_more_thing: + heroku_scheduler: mark_all: пометить все ready: Все готово! refresh: обновить diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 60d2b080a..117528412 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -142,6 +142,9 @@ sv: as_read: som lästa click_to_read: (klicka för att läsa) description: Vi hämtar några berättelser åt dig, ge oss en stund. + heroku_hourly_task: + heroku_one_more_thing: + heroku_scheduler: mark_all: markera alla ready: Ok, det är klart! refresh: uppdatera diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 06bf88d34..742b77c43 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -142,6 +142,9 @@ tr: as_read: okundu olarak click_to_read: (okumak icin tikla) description: Sana okuman icin hikayeler getirecegiz, bize biraz zaman ver. + heroku_hourly_task: + heroku_one_more_thing: + heroku_scheduler: mark_all: hepsini isaretle ready: Tamam hazir! refresh: yenile diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index c25c826ba..3e02eeaff 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -142,6 +142,9 @@ zh-CN: as_read: 已读 click_to_read: (点击阅读) description: 我们正在努力的加载您订阅的内容,请稍等。 + heroku_hourly_task: + heroku_one_more_thing: + heroku_scheduler: mark_all: 全部标为 ready: 好了,我想你已经准备好了~ refresh: 刷新 From 756ed157e21c5f4f3872fcdc5c66f02f7decf86a Mon Sep 17 00:00:00 2001 From: Jacob Krall Date: Thu, 7 Aug 2014 14:35:59 -0500 Subject: [PATCH 0042/1107] app.json: use main repository URL for logo --- app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app.json b/app.json index 030eea0ab..d8ca2de19 100644 --- a/app.json +++ b/app.json @@ -1,7 +1,7 @@ { "name": "Stringer", "description": "A self-hosted, anti-social RSS reader.", - "logo": "https://raw.githubusercontent.com/swanson/testing-hb/master/screenshots/logo.png", + "logo": "https://raw.githubusercontent.com/swanson/stringer/master/screenshots/logo.png", "keywords": [ "RSS", "Ruby" @@ -24,4 +24,4 @@ "addons": [ "scheduler:standard" ] -} \ No newline at end of file +} From 3f27bdbc70cb96bc6dbd86309dda3d164dd035be Mon Sep 17 00:00:00 2001 From: Jacob Krall Date: Thu, 7 Aug 2014 15:13:03 -0500 Subject: [PATCH 0043/1107] svg badges using shield.io --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e7f4fd723..d28167ddf 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Stringer -[![Build Status](https://travis-ci.org/swanson/stringer.png)](https://travis-ci.org/swanson/stringer) -[![Code Climate](https://codeclimate.com/github/swanson/stringer.png)](https://codeclimate.com/github/swanson/stringer) -[![Coverage Status](https://coveralls.io/repos/swanson/stringer/badge.png?branch=master)](https://coveralls.io/r/swanson/stringer) +[![Build Status](http://img.shields.io/travis/swanson/stringer.svg)](https://travis-ci.org/swanson/stringer) +[![Code Climate](http://img.shields.io/codeclimate/github/swanson/stringer.svg)](https://codeclimate.com/github/swanson/stringer) +[![Coverage Status](http://img.shields.io/coveralls/swanson/stringer.svg)](https://coveralls.io/r/swanson/stringer) ### A self-hosted, anti-social RSS reader. From a55eeb304bc60aa5c4de4ccaeb9b1ac0c96dfbb6 Mon Sep 17 00:00:00 2001 From: Pascal Widdershoven Date: Tue, 12 Aug 2014 10:44:53 +0200 Subject: [PATCH 0044/1107] Change FetchFeed consturctor to use named parameters This way you can initialize the FetchFeed class with passing a custom logger, but without having to pass a parser. --- app/tasks/fetch_feed.rb | 4 ++-- spec/tasks/fetch_feed_spec.rb | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/tasks/fetch_feed.rb b/app/tasks/fetch_feed.rb index dbd21a9b8..4c86c60e7 100644 --- a/app/tasks/fetch_feed.rb +++ b/app/tasks/fetch_feed.rb @@ -8,9 +8,9 @@ class FetchFeed USER_AGENT = "Stringer (https://github.com/swanson/stringer)" - def initialize(feed, feed_parser = Feedjira::Feed, logger = nil) + def initialize(feed, parser: Feedjira::Feed, logger: nil) @feed = feed - @parser = feed_parser + @parser = parser @logger = logger end diff --git a/spec/tasks/fetch_feed_spec.rb b/spec/tasks/fetch_feed_spec.rb index 18366d6d0..86a8e76a1 100644 --- a/spec/tasks/fetch_feed_spec.rb +++ b/spec/tasks/fetch_feed_spec.rb @@ -21,7 +21,7 @@ StoryRepository.should_not_receive(:add) - FetchFeed.new(daring_fireball, parser) + FetchFeed.new(daring_fireball, parser: parser) end end @@ -34,7 +34,7 @@ StoryRepository.should_not_receive(:add) - FetchFeed.new(daring_fireball, parser).fetch + FetchFeed.new(daring_fireball, parser: parser).fetch end end @@ -52,14 +52,14 @@ StoryRepository.should_receive(:add).with(new_story, daring_fireball) StoryRepository.should_not_receive(:add).with(old_story, daring_fireball) - FetchFeed.new(daring_fireball, fake_parser).fetch + FetchFeed.new(daring_fireball, parser: fake_parser).fetch end it "should update the last fetched time for the feed" do FeedRepository.should_receive(:update_last_fetched) .with(daring_fireball, now) - FetchFeed.new(daring_fireball, fake_parser).fetch + FetchFeed.new(daring_fireball, parser: fake_parser).fetch end end @@ -71,7 +71,7 @@ FeedRepository.should_receive(:set_status) .with(:green, daring_fireball) - FetchFeed.new(daring_fireball, parser).fetch + FetchFeed.new(daring_fireball, parser: parser).fetch end it "sets the status to red if things go wrong" do @@ -80,7 +80,7 @@ FeedRepository.should_receive(:set_status) .with(:red, daring_fireball) - FetchFeed.new(daring_fireball, parser).fetch + FetchFeed.new(daring_fireball, parser: parser).fetch end end end From 1545cebe93afeead6aa760ab968a2a7c94e8223b Mon Sep 17 00:00:00 2001 From: Matt Swanson Date: Tue, 12 Aug 2014 12:54:40 -0400 Subject: [PATCH 0045/1107] Update translations --- config/locales/sv.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 117528412..41c85af63 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -32,11 +32,11 @@ sv: title: Behöver du nya berättelser? edit: fields: - feed_name: - feed_url: - submit: + feed_name: Feednamn + feed_url: Feed-URL + submit: Spara flash: - updated_successfully: + updated_successfully: Uppdaterade feeden åt dig! index: add: lägga till add_some_feeds: Hej, du borde %{add} några feeds. @@ -96,7 +96,7 @@ sv: shortcuts: keys: a: Lägg till en feed - f: + f: Gå till feed-sidan jk: Nästa/föregående berättelse left: Föregående sida m: Markera som läst/oläst @@ -142,9 +142,9 @@ sv: as_read: som lästa click_to_read: (klicka för att läsa) description: Vi hämtar några berättelser åt dig, ge oss en stund. - heroku_hourly_task: - heroku_one_more_thing: - heroku_scheduler: + heroku_hourly_task: Du behöver lägga till ett timvis återkommande jobb för att ladda nya berättelser. + heroku_one_more_thing: En sak till... + heroku_scheduler: Gå till Heroku Scheduler och lägg till detta jobb mark_all: markera alla ready: Ok, det är klart! refresh: uppdatera From 15a81702290e627fcd85588e4014d0bae9e41138 Mon Sep 17 00:00:00 2001 From: Pascal Widdershoven Date: Mon, 11 Aug 2014 19:54:50 +0200 Subject: [PATCH 0046/1107] Add integration test for feed importing Covers: - Initial import - Update where no new entries are available - Update where new entries are available --- Gemfile | 1 + Gemfile.lock | 9 +++ spec/integration/feed_importing_spec.rb | 62 ++++++++++++++ .../feeds/feed01_valid_feed/feed.xml | 80 +++++++++++++++++++ .../feeds/feed01_valid_feed/feed_updated.xml | 80 +++++++++++++++++++ spec/support/feed_server.rb | 15 ++++ 6 files changed, 247 insertions(+) create mode 100644 spec/integration/feed_importing_spec.rb create mode 100644 spec/sample_data/feeds/feed01_valid_feed/feed.xml create mode 100644 spec/sample_data/feeds/feed01_valid_feed/feed_updated.xml create mode 100644 spec/support/feed_server.rb diff --git a/Gemfile b/Gemfile index 6d535f8fd..ecd70e909 100644 --- a/Gemfile +++ b/Gemfile @@ -19,6 +19,7 @@ group :development, :test do gem "rspec", "~> 2.14", ">= 2.14.1" gem "rspec-html-matchers", "~> 0.4.3" gem "shotgun", "~> 0.9.0" + gem "capybara" end gem "activerecord", "~> 4.0" diff --git a/Gemfile.lock b/Gemfile.lock index 87ad7486f..b80b46e1d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -30,6 +30,12 @@ GEM byebug (2.5.0) columnize (~> 0.3.6) debugger-linecache (~> 1.2.0) + capybara (2.4.1) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) coderay (1.1.0) columnize (0.3.6) coveralls (0.7.0) @@ -150,6 +156,8 @@ GEM rack raindrops (~> 0.7) will_paginate (3.0.5) + xpath (2.0.0) + nokogiri (~> 1.3) PLATFORMS ruby @@ -158,6 +166,7 @@ DEPENDENCIES activerecord (~> 4.0) arel! bcrypt-ruby (~> 3.1.2) + capybara coveralls (~> 0.7) delayed_job (~> 4.0) delayed_job_active_record (~> 4.0) diff --git a/spec/integration/feed_importing_spec.rb b/spec/integration/feed_importing_spec.rb new file mode 100644 index 000000000..ce44b4f6c --- /dev/null +++ b/spec/integration/feed_importing_spec.rb @@ -0,0 +1,62 @@ +require "spec_helper" +require "support/active_record" +require "support/feed_server" +require "capybara" +require "capybara/server" + +app_require "tasks/fetch_feed" + +describe "Feed importing" do + before(:all) do + @server = FeedServer.new + end + + let(:feed) do + Feed.create( + name: "Example feed", + last_fetched: Time.new(2014, 1, 1), + url: @server.url + ) + end + + describe "Valid feed" do + describe "Importing for the first time" do + it "imports all entries" do + @server.response = sample_data('feeds/feed01_valid_feed/feed.xml') + expect { fetch_feed(feed) }.to change{ feed.stories.count }.to(5) + end + end + + describe "Importing for the second time" do + before(:each) do + @server.response = sample_data('feeds/feed01_valid_feed/feed.xml') + fetch_feed(feed) + end + + context "no new entries" do + it "does not create new stories" do + @server.response = sample_data('feeds/feed01_valid_feed/feed.xml') + expect { fetch_feed(feed) }.to_not change{ feed.stories.count } + end + end + + context "new entries" do + it "creates new stories" do + @server.response = sample_data('feeds/feed01_valid_feed/feed_updated.xml') + expect { fetch_feed(feed) }.to change{ feed.stories.count }.by(1).to(6) + end + end + end + end +end + +def sample_data(path) + File.new(File.join("spec", "sample_data", path)).read +end + +def fetch_feed(feed) + logger = Logger.new(STDOUT) + logger.level = Logger::DEBUG + + FetchFeed.new(feed, logger: logger).fetch +end diff --git a/spec/sample_data/feeds/feed01_valid_feed/feed.xml b/spec/sample_data/feeds/feed01_valid_feed/feed.xml new file mode 100644 index 000000000..f7853f31d --- /dev/null +++ b/spec/sample_data/feeds/feed01_valid_feed/feed.xml @@ -0,0 +1,80 @@ + + + + + + MacRumors: Mac News and Rumors - Front Page + http://www.macrumors.com + the mac news you care about + en + Fri, 15 Aug 2014 17:38:02 GMT + Fri, 15 Aug 2014 17:38:02 GMT + 2 + hourly + 1 + 2014-08-15T17:39:13Z + + + + + Apple Working to Remedy Labor Violations Found at Quanta Factories + http://www.macrumors.com/2014/08/15/apple-labor-violations-quanta/ + The Fair Labor Association (FLA) today published <a href="http://www.fairlabor.org/2013-apple-quanta-shanghai-changshu">a new report </a>examining two factories operated by Apple-supplier Quanta Computer, finding several code violations related to working hours, recruitment policies, compensation, health and safety, and more in August of 2013 [<a href="http://www.fairlabor.org/sites/default/files/documents/reports/august-2014-apple-quanta-executive-summary_0.pdf">PDF</a>] (via <a href="http://techcrunch.com/2014/08/15/the-fair-labor-association-delivers-its-findings-on-two-apple-supplier-facilities/"><em>TechCrunch</em></a>). <br/> <br/> Factories examined included a Quanta facility in Shanghai and one in Changshu. Quanta is a long-time Apple partner that manufactures Apple's MacBook Air and much of the rest of the company's Mac lineup. <br/> <br/> Violations were found in both locations, with some of the more egregious issues including verbal abuse by supervisors, a hiring fee charged to workers by a broker or labor dispatch agent and long working hours. According to the report, 62 percent of workers in Changshu received no rest day for much of Q4 2012, working as many as 16 days in a row. <br/> <br/> <img src="http://cdn.macrumors.com/article-new/2014/08/supplierscore.jpg" alt="supplierscore" width="800" height="228" class="aligncenter size-full wp-image-419578" /><center><em>Overall score summary of management functions at Changshu</em></center> <br/> Many workers were also underpaid for sick leave and may have been uncompensated for up to an hour of work each day, based on clock in and clock out times. Some workers were forced into joining the All China Federation of Trade Unions, and there were several safety violations. <br/> <br/> Both of the factories fell short of the local requirements for indoor air quality, and neither had easy access to a shower/eyewash station in case of emergency. There was no active worker participation in the Employee Health and Safety committees, and flammable and toxic substances were stored improperly at Shanghai while chemicals at Changshu were not properly monitored. <br/> <br/> The Fair Labor Association provided a number of recommendations to improve conditions at the factory, and according to the report, Apple is using the recommendations to work with Quanta to fix each code violation. Apple released a statement on the FLA's Quanta inspection, stating that it has worked closely with Quanta to bring improvements to working conditions.<blockquote>Our suppliers must live up to the toughest standards in the industry if they want to keep doing business with Apple, which is the first and only technology company to be admitted to the Fair Labor Association. We are committed to providing safe and fair working conditions for everyone in our supply chain. <br/> <br/> Last year we conducted 451 comprehensive, in-person audits deep into our supply chain so we could uncover problems and work with our suppliers to fix them. We track and report the weekly working hours for more than 1 million workers, and our 18-month Apple Supplier EHS Academy training program is raising the bar for environment, health and safety management in the industry. <br/> <br/> The Quanta facilities inspected by the FLA last year were included in our 2014 Supplier Responsibility report, which we released in February. Our own experts have audited these sites 16 times, most recently last month. <br/> <br/> In the year since the FLA’s visit, we have worked closely with Quanta to drive meaningful improvements in areas identified by both the FLA and Apple. Apple conducted four follow-up inspections on top of the annual audits of both facilities, to ensure the needed corrections are in place. <br/> <br/> This year, through the end of July, Quanta has averaged 86 percent compliance with our 60-hour workweek. Excessive overtime is not in anyone’s best interest, and we will continue to work closely with Quanta and our other suppliers to prevent it.</blockquote>Apple initially signed up for factory assessments by the Fair Labor Association <a href="http://www.macrumors.com/2012/01/13/apple-partners-with-labor-group-to-monitor-workplace-conditions-at-suppliers-factories/">back in 2012</a>, following a rash of <a href="http://www.macrumors.com/2010/05/26/apple-independently-evaluating-foxconns-response-to-suicides-at-manufacturing-plant/">worker suicides</a> at Foxconn, the factory responsible for assembling many of Apple's mobile devices. The FLA has since helped to <a href="http://www.macrumors.com/2013/12/12/foxconn-and-apple-make-strides-towards-improving-work-hours-but-still-violate-chinese-limits/">improve working conditions</a> in several of Apple's factories, with Apple aiming to bring all workplace compliance standards in line with the FLA's guidelines. <br/> <br/> Apple also maintains a <a href="http://www.macrumors.com/2012/01/13/apple-releases-2012-supplier-responsibility-progress-report-and-supplier-list/">Supplier Responsibility team</a> that audits supply chain facilities to ensure compliance with Apple's strict code of conduct preventing underage labor and providing safe, comfortable living conditions for workers. An additional Supplier Responsiblity academic board also evaluates Apple's labor policies and practices and researches labor standards within the supply chain to create ethical working conditions wherever Apple products are produced.<br/> <br/> <br/> <b>Recent Mac and iOS Blog Stories</b><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/apple-beats-spike-lee-documentary/">Beats Music Creates Documentary On Spike Lee&#039;s &#039;Do the Right Thing&#039;</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/stacksocial-maclovin-bundle/">StackSocial Offers 8 Mac Apps in &#039;MacLovin&#039; Bundle for &#36;39.99</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/apple-seeds-new-safari-betas-to-developers/">Apple Releases Safari 7.0.6 and Safari 6.1.6 With Security Fixes for Mavericks/Mountain Lion</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/mailbox-new-email-management-features/">Mailbox for iOS Gains New Email Management Features, Languages</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/os-x-mavericks-10-9-5-build-13f14/">Apple Seeds OS X Mavericks 10.9.5 Build 13F14 to Developers</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/amazon-local-register-card-reader/">Amazon Launches iOS Compatible &#039;Amazon Local Register&#039; Card Reader</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/12/skylanders-trap-team-controller-set/">Full &#039;Skylanders Trap Team&#039; Game Coming to iPad With Bluetooth Controller, Energy Portal</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/08/apple-iwatch-september-gruber/">Apple to Introduce iWatch in September Suggests Apple Journalist John Gruber [Updated]</a><br/><img width='1' height='1' src='http://rss.feedsportal.com/c/35070/f/648327/s/3d8b2e60/sc/28/mf.gif' border='0'/><br clear='all'/><br/><br/><a href="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/rc/1/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/rc/1/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/rc/2/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/rc/2/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/rc/3/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/a2.htm"><img src="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/a2t.img" border="0"/><div class="feedflare"> +<a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=0wQ4Gbmj5Ac:XD-Tnjqu3Zw:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=0wQ4Gbmj5Ac:XD-Tnjqu3Zw:6W8y8wAjSf4"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=6W8y8wAjSf4" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=0wQ4Gbmj5Ac:XD-Tnjqu3Zw:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=qj6IDK7rITs" border="0"></img></a> +</div><img src="http://feeds.feedburner.com/~r/MacRumors-Front/~4/0wQ4Gbmj5Ac" height="1" width="1"/> + Front Page + Fri, 15 Aug 2014 17:36:37 GMT + http://forums.macrumors.com/showthread.php?t=1765267 + http://www.macrumors.com/2014/08/15/apple-labor-violations-quanta/ + Juli Clover + + + Apple Adds Five Vice Presidents, Including Two Women, to 'Apple Leadership' Press Page + http://www.macrumors.com/2014/08/15/apple-leadership-press-page-updates/ + Apple today updated its <a href="http://www.apple.com/pr/bios/">Apple Leadership</a> press page to add the bios of five vice presidents, including Paul Deneve, Lisa Jackson, Joel Podolny, Johny Srouji, and Denise Young Smith. <br/> <br/> The inclusion of several vice presidents on the executive team is a new move for the company, as the page previously only listed the company's lineup of senior vice presidents. <br/> <br/> <img src="http://cdn.macrumors.com/article-new/2014/08/applevps.jpg" alt="applevps" width="600" height="353" class="aligncenter size-full wp-image-419575" /> <br/> Since taking over as CEO, Tim Cook has aimed to share the spotlight with his employees, letting company leaders like Eddy Cue, Craig Federighi, and Jony Ive take part in <a href="http://www.macrumors.com/2013/09/25/jony-ive-and-craig-federighi-talk-collaboration-in-full-businessweek-interview/">press interviews</a> and lead Apple events. The addition of five new VPs to the press roster suggests Cook and the team at Apple are more open than ever, recognizing the efforts of an even larger number of key employees. <br/> <br/> Of the new executives joining the page, <a href="http://www.macrumors.com/2013/07/02/apple-to-hire-former-yves-saint-laurent-ceo-paul-deneve/">Paul Deneve</a> and <a href="http://www.macrumors.com/2013/05/28/apple-hires-former-epa-chief-lisa-jackson-to-oversee-environmental-efforts/">Lisa Jackson</a> are relatively new hires, joining Apple in 2013. Deneve oversees special projects, while Jackson has a public-facing job overseeing Apple's environmental efforts. Joel Podolny, dean of Apple University and Denise Young Smith, who oversees Apple's human resources, were <a href="http://www.macrumors.com/2014/02/11/podolny-apple-university/">promoted to their roles</a> early in 2014. The remaining VP, Johny Srouji, oversees hardware technologies, a role he has held for just over a year. <br/> <br/> The new additions to the executive press page also spotlight two additional women who hold important leadership roles at Apple, bringing the total number of females on the page to three, with Angela Ahrendts. Apple has faced criticism for its <a href="http://www.macrumors.com/2014/01/06/apple-updates-corporate-bylaws-on-diversity-following-criticism-over-lack-of-female-leaders/">lack of female leadership</a> in the past, which Cook has been aiming to remedy. According to diversity numbers released earlier this week, 72 percent of Apple employees in leadership roles are male. <br/> <br/> <small>Note: Due to the nature of the discussion regarding this topic, the discussion thread is located in our <a href="http://forums.macrumors.com/forumdisplay.php?f=47">Politics, Religion, Social Issues</a> forum. All forum members and site visitors are welcome to read and follow the thread, but posting is limited to forum members with at least 100 posts.</small><br/> <br/> <br/> <b>Recent Mac and iOS Blog Stories</b><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/apple-beats-spike-lee-documentary/">Beats Music Creates Documentary On Spike Lee&#039;s &#039;Do the Right Thing&#039;</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/stacksocial-maclovin-bundle/">StackSocial Offers 8 Mac Apps in &#039;MacLovin&#039; Bundle for &#36;39.99</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/apple-seeds-new-safari-betas-to-developers/">Apple Releases Safari 7.0.6 and Safari 6.1.6 With Security Fixes for Mavericks/Mountain Lion</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/mailbox-new-email-management-features/">Mailbox for iOS Gains New Email Management Features, Languages</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/os-x-mavericks-10-9-5-build-13f14/">Apple Seeds OS X Mavericks 10.9.5 Build 13F14 to Developers</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/amazon-local-register-card-reader/">Amazon Launches iOS Compatible &#039;Amazon Local Register&#039; Card Reader</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/12/skylanders-trap-team-controller-set/">Full &#039;Skylanders Trap Team&#039; Game Coming to iPad With Bluetooth Controller, Energy Portal</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/08/apple-iwatch-september-gruber/">Apple to Introduce iWatch in September Suggests Apple Journalist John Gruber [Updated]</a><br/><img width='1' height='1' src='http://rss.feedsportal.com/c/35070/f/648327/s/3d8b03fc/sc/21/mf.gif' border='0'/><br clear='all'/><br/><br/><a href="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/rc/1/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/rc/2/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/a2t.img" border="0"/><div class="feedflare"> +<a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=pGJ2BCCYpIg:2bXK9R8yazc:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=pGJ2BCCYpIg:2bXK9R8yazc:6W8y8wAjSf4"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=6W8y8wAjSf4" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=pGJ2BCCYpIg:2bXK9R8yazc:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=qj6IDK7rITs" border="0"></img></a> +</div><img src="http://feeds.feedburner.com/~r/MacRumors-Front/~4/pGJ2BCCYpIg" height="1" width="1"/> + Front Page + Fri, 15 Aug 2014 17:11:42 GMT + http://forums.macrumors.com/showthread.php?t=1765257 + http://www.macrumors.com/2014/08/15/apple-leadership-press-page-updates/ + Juli Clover + + + Earphones Filled With Health and Fitness Sensors Ready to Flood the Wearables Market + http://www.macrumors.com/2014/08/15/health-fitness-sensor-earphones/ + As Apple moves closer to launching its rumored health and fitness oriented iWatch, there has been significant focus on the types of sensors Apple might be looking to deploy. Among the intriguing options have been sensor-equipped earphones, which Apple has filed patent applications on in the past and which saw renewed attention following the posting of an <a href="http://www.macrumors.com/2014/05/01/earpods-biometric-sensors/">unfounded rumor</a> about EarPods with embedded biometric sensors earlier this year. <br/> <br/> Earphones with health and fitness sensors do continue to be a topic of interest, and other manufacturers are preparing to bring their own products to the wearables market later this year. One biometric headphone attracting attention is <a href="http://newsroom.intel.com/community/intel_newsroom/blog/2014/08/14/intel-and-sms-audio-to-supercharge-fitness-wearables">a new offering</a> from Intel and SMS Audio, which has musician 50 Cent as a co-founder (via <a href="http://recode.net/2014/08/14/gimme-a-beat-intel-50-cent-pair-up-on-heart-rate-headphones/"><em>Re/Code</em></a>). <br/> <br/> <img src="http://cdn.macrumors.com/article-new/2014/08/intel-ear-buds.jpeg" alt="intel-ear-buds" width="640" height="480" class="aligncenter size-full wp-image-419561" /> <br/> The SMS Audio BioSport In-Ear Headphones include heart rate monitoring powered by Intel technology and feature SMS Audio's high-quality sound, comfort and fashion. The ear buds will use a combination of sensors, including an optical light sensor, to measure both resting and active heart rates. Powered by the smartphone's audio jack, the headphones will share their data with a variety of third-party apps, with RunKeeper integration available at launch. <br/> <br/> Intel and SMS Audio may be among the biggest names entering the sensor headphone market, but they are not alone. Earlier this year, LG <a href="http://www.lg.com/us/fitness-activity-trackers/lg-FR74-heart-rate-monitor">released</a> its own Bluetooth-connected, heart-rate-measuring earphones with <a href="http://recode.net/2014/06/16/lgs-heart-rate-earphones-might-not-win-your-heart/">mixed reviews</a>. Hitting the market later this year are wireless <a href="https://www.kickstarter.com/projects/freewavz/freewavz-smart-earphones-with-built-in-fitness-mon">Smart Earphones from FreeWavz</a>, which monitors heart rate and other key fitness metrics. The Kickstarter-backed product reached its funding goal earlier this month and has a target ship date of October 2014. <br/> <br/> Beyond the ultimately false rumor of EarPods with biometric sensors and Apple's patent applications dating back a number of years, the company also <a href="http://www.macrumors.com/2014/05/01/apple-hires-another-wearables-expert/">recently hired</a> former MIT researcher Eric Winokur as a Sensing Hardware engineer. Winokur is known for his work on wearable medical devices, including ear-worn sensors for cardiovascular monitoring. It is not known, however, whether Winokur is continuing his work on ear-worn sensors at Apple or if he is contributing more broadly to the <a href="http://www.macrumors.com/2014/02/13/apple-biometrics-team-lamego/">biometrics team</a> working on the iWatch. <br/> <br/> <br/> <b>Recent Mac and iOS Blog Stories</b><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/apple-beats-spike-lee-documentary/">Beats Music Creates Documentary On Spike Lee&#039;s &#039;Do the Right Thing&#039;</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/stacksocial-maclovin-bundle/">StackSocial Offers 8 Mac Apps in &#039;MacLovin&#039; Bundle for &#36;39.99</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/apple-seeds-new-safari-betas-to-developers/">Apple Releases Safari 7.0.6 and Safari 6.1.6 With Security Fixes for Mavericks/Mountain Lion</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/mailbox-new-email-management-features/">Mailbox for iOS Gains New Email Management Features, Languages</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/os-x-mavericks-10-9-5-build-13f14/">Apple Seeds OS X Mavericks 10.9.5 Build 13F14 to Developers</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/amazon-local-register-card-reader/">Amazon Launches iOS Compatible &#039;Amazon Local Register&#039; Card Reader</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/12/skylanders-trap-team-controller-set/">Full &#039;Skylanders Trap Team&#039; Game Coming to iPad With Bluetooth Controller, Energy Portal</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/08/apple-iwatch-september-gruber/">Apple to Introduce iWatch in September Suggests Apple Journalist John Gruber [Updated]</a><br/><img width='1' height='1' src='http://rss.feedsportal.com/c/35070/f/648327/s/3d8a861d/sc/28/mf.gif' border='0'/><br clear='all'/><br/><br/><a href="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/rc/1/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/rc/1/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/rc/2/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/rc/2/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/rc/3/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/a2.htm"><img src="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/a2t.img" border="0"/><div class="feedflare"> +<a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=3SJMnGU-uh0:k2Wk9HJTvCM:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=3SJMnGU-uh0:k2Wk9HJTvCM:6W8y8wAjSf4"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=6W8y8wAjSf4" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=3SJMnGU-uh0:k2Wk9HJTvCM:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=qj6IDK7rITs" border="0"></img></a> +</div><img src="http://feeds.feedburner.com/~r/MacRumors-Front/~4/3SJMnGU-uh0" height="1" width="1"/> + Front Page + Fri, 15 Aug 2014 15:08:17 GMT + http://forums.macrumors.com/showthread.php?t=1765221 + http://www.macrumors.com/2014/08/15/health-fitness-sensor-earphones/ + Kelly Hodgkins + + + Apple Now Using China Telecom as Data Center Provider in China + http://www.macrumors.com/2014/08/15/apple-china-telecom-data-center/ + <img src="http://cdn.macrumors.com/article-new/2014/08/china_telecom_logo-250x250.png" alt="china_telecom_logo" width="250" height="250" class="alignright size-medium wp-image-419553" /> Apple has officially added China Telecom as a data center provider in China, <a href="http://blogs.wsj.com/digits/2014/08/15/apple-adds-china-telecom-as-data-center-provider/?mod=rss_Technology">reports</a> <em>The Wall Street Journal</em>. The move comes after 15 months of "stringent tests and evaluation" by the Fuzhou city government, as Apple states that all data stored on the servers is encrypted. According to <em><a href="http://www.reuters.com/article/2014/08/15/us-apple-data-china-idUSKBN0GF0N720140815?feedType=RSS&#38;feedName=technologyNews">Reuters</a></em>, Apple says the new data center will help improve the speed and reliability of iCloud and the iTunes Store in the region. <blockquote>"Apple takes user security and privacy very seriously. We have added China Telecom to our list of data center providers to increase bandwidth and improve performance for our customers in mainland China," it said.</blockquote> The move could also help ease tensions between Apple and China as of late, as the country recently <a href="http://www.macrumors.com/2014/07/11/chinese-media-iphone-location-tracking-security/">deemed</a> iOS' location tracking services a "national security concern." Apple <a href="http://www.macrumors.com/2014/07/12/apple-responds-china-ios-location-tracking/">responded</a> to those claims reiterating its commitment to privacy and stating that its Location Services exist to aide navigation features. <br/> <br/> China has become an important market for Apple, as the company has looked to improve its presence in the country as of late. Late last year, the company <a href="http://www.macrumors.com/2013/12/04/apple-finally-signs-deal-with-china-mobile-for-december-iphone-launch/">started</a> selling the iPhone on China Mobile, the country's biggest carrier, and opened more retail stores throughout the region. CEO Tim Cook has also made a number of <a href="http://www.macrumors.com/2012/03/28/apple-ceo-tim-cook-meets-with-beijing-mayor-chinese-vice-premier-during-visit/">visits</a> to China, meeting with Bejing's mayor and the Chinese Vice Premier to discuss opportunities. <br/> <br/> <br/> <b>Recent Mac and iOS Blog Stories</b><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/apple-beats-spike-lee-documentary/">Beats Music Creates Documentary On Spike Lee&#039;s &#039;Do the Right Thing&#039;</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/stacksocial-maclovin-bundle/">StackSocial Offers 8 Mac Apps in &#039;MacLovin&#039; Bundle for &#36;39.99</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/apple-seeds-new-safari-betas-to-developers/">Apple Releases Safari 7.0.6 and Safari 6.1.6 With Security Fixes for Mavericks/Mountain Lion</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/mailbox-new-email-management-features/">Mailbox for iOS Gains New Email Management Features, Languages</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/os-x-mavericks-10-9-5-build-13f14/">Apple Seeds OS X Mavericks 10.9.5 Build 13F14 to Developers</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/amazon-local-register-card-reader/">Amazon Launches iOS Compatible &#039;Amazon Local Register&#039; Card Reader</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/12/skylanders-trap-team-controller-set/">Full &#039;Skylanders Trap Team&#039; Game Coming to iPad With Bluetooth Controller, Energy Portal</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/08/apple-iwatch-september-gruber/">Apple to Introduce iWatch in September Suggests Apple Journalist John Gruber [Updated]</a><br/><img width='1' height='1' src='http://rss.feedsportal.com/c/35070/f/648327/s/3d88e7a5/sc/28/mf.gif' border='0'/><br clear='all'/><br/><br/><a href="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/rc/1/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/rc/1/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/rc/2/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/rc/2/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/rc/3/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/a2.htm"><img src="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/a2t.img" border="0"/><div class="feedflare"> +<a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=5orxG6suAvU:4cJ4URr9D-c:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=5orxG6suAvU:4cJ4URr9D-c:6W8y8wAjSf4"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=6W8y8wAjSf4" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=5orxG6suAvU:4cJ4URr9D-c:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=qj6IDK7rITs" border="0"></img></a> +</div><img src="http://feeds.feedburner.com/~r/MacRumors-Front/~4/5orxG6suAvU" height="1" width="1"/> + Front Page + Fri, 15 Aug 2014 11:07:04 GMT + http://forums.macrumors.com/showthread.php?t=1765148 + http://www.macrumors.com/2014/08/15/apple-china-telecom-data-center/ + Richard Padilla + + + Photos Claiming to Be of New Lightning Cable with Reversible USB Connector Surface + http://www.macrumors.com/2014/08/15/new-lightning-cable-reversible-usb/ + Apple may be shipping a new Lightning cable that features a reversible USB connector with forthcoming iOS devices, <a href="http://www.dianxinshouji.com.cn/archives/1017">reports</a> Chinese website Dianxinshouji.com (<a href="http://translate.google.com/translate?js=n&#38;sl=auto&#38;tl=en&#38;u=http://www.dianxinshouji.com.cn/archives/1017">Google Translate</a>, via <em><a href="http://www.nowhereelse.fr/iphone-6-cable-usb-reversible-100226/">Nowhereelse.fr</a></em>). <br/> <br/> <img src="http://cdn.macrumors.com/article-new/2014/08/reversible_lightning_1-800x374.jpg" alt="reversible_lightning_1" width="800" height="374" class="aligncenter size-large wp-image-419547" /> <br/> The source shares a few photos of the new cables said to be from Apple supplier Foxconn, with the images showing a USB connector that is attached to the center of its metal casing. By comparison, the USB connector on Apple's current Lightning cable attach against the bottom of the metal housing's inner surface. <br/> <br/> <img src="http://cdn.macrumors.com/article-new/2014/08/reversible_lightning_2-800x298.jpg" alt="reversible_lightning_2" width="800" height="298" class="aligncenter size-large wp-image-419548" /> <br/> While it is unable to tell for sure whether these cables are legitimate or not, it is possible that Apple could ship new Lightning cables to match the forthcoming USB 3.1 cables that will soon come with newer smartphones. As <a href="http://www.pocket-lint.com/news/130338-what-is-type-c-usb-3-1-faster-charging-quicker-data-smaller-mobiles-and-the-death-of-ac-laptop-chargers">revealed</a> last week by the USB 3.0 Promoter Group, the USB 3.1 Type-C cable comes with reversible ends and will start shipping next year. An Apple patent for a reversible USB connector also <a href="http://appft1.uspto.gov/netacgi/nph-Parser?Sect1=PTO1&#38;Sect2=HITOFF&#38;d=PG01&#38;p=1&#38;u=%2Fnetahtml%2FPTO%2Fsrchnum.html&#38;r=1&#38;f=G&#38;l=50&#38;s1=%2220140206209%22.PGNR.&#38;OS=DN/20140206209&#38;RS=DN/20140206209">surfaced</a> last month, perhaps further indicating that the company will look to equip its newer devices with new Lightning cables at some point. <br/> <br/> Furthermore, a <a href="http://www.macrumors.com/2014/05/13/hd-audio-ios-8-new-in-ear-headphones-lightning/">report</a> this past May from <em>Mac Otakara</em> claimed that Apple is preparing an upgraded Lightning cable to accommodate high-definition playback on Made for iPhone audio accessories, which may include a next-generation version of its In-Ear Headphones. Apple also <a href="http://www.macrumors.com/2014/06/03/lighting-cable-headphone-mfi/">introduced</a> Lightning Cable MFi specifications for headphones in June, which could also be integrated with newer Lightning cables. <br/> <br/> Apple is expected to launch the <a href="http://www.macrumors.com/roundup/iphone-6/">iPhone 6</a>, next-generation <a href="http://www.macrumors.com/roundup/ipad-mini/">Retina iPad mini</a> and <a href="http://www.macrumors.com/roundup/ipad/">iPad Air 2</a> by the end of this year, as a new Lightning cable could technically be packaged with those devices. <br/> <br/> <br/> <b>Recent Mac and iOS Blog Stories</b><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/apple-beats-spike-lee-documentary/">Beats Music Creates Documentary On Spike Lee&#039;s &#039;Do the Right Thing&#039;</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/stacksocial-maclovin-bundle/">StackSocial Offers 8 Mac Apps in &#039;MacLovin&#039; Bundle for &#36;39.99</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/apple-seeds-new-safari-betas-to-developers/">Apple Releases Safari 7.0.6 and Safari 6.1.6 With Security Fixes for Mavericks/Mountain Lion</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/mailbox-new-email-management-features/">Mailbox for iOS Gains New Email Management Features, Languages</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/os-x-mavericks-10-9-5-build-13f14/">Apple Seeds OS X Mavericks 10.9.5 Build 13F14 to Developers</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/amazon-local-register-card-reader/">Amazon Launches iOS Compatible &#039;Amazon Local Register&#039; Card Reader</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/12/skylanders-trap-team-controller-set/">Full &#039;Skylanders Trap Team&#039; Game Coming to iPad With Bluetooth Controller, Energy Portal</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/08/apple-iwatch-september-gruber/">Apple to Introduce iWatch in September Suggests Apple Journalist John Gruber [Updated]</a><br/><img width='1' height='1' src='http://rss.feedsportal.com/c/35070/f/648327/s/3d8867e2/sc/28/mf.gif' border='0'/><br clear='all'/><br/><br/><a href="http://da.feedsportal.com/r/204366598199/u/49/f/648327/c/35070/s/3d8867e2/sc/28/rc/1/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366598199/u/49/f/648327/c/35070/s/3d8867e2/sc/28/rc/1/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366598199/u/49/f/648327/c/35070/s/3d8867e2/sc/28/rc/2/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366598199/u/49/f/648327/c/35070/s/3d8867e2/sc/28/rc/2/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366598199/u/49/f/648327/c/35070/s/3d8867e2/sc/28/rc/3/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366598199/u/49/f/648327/c/35070/s/3d8867e2/sc/28/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/204366598199/u/49/f/648327/c/35070/s/3d8867e2/sc/28/a2.htm"><img src="http://da.feedsportal.com/r/204366598199/u/49/f/648327/c/35070/s/3d8867e2/sc/28/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/204366598199/u/49/f/648327/c/35070/s/3d8867e2/sc/28/a2t.img" border="0"/><div class="feedflare"> +<a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=ZNrYr03avVI:4RJ4Gljljlg:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=ZNrYr03avVI:4RJ4Gljljlg:6W8y8wAjSf4"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=6W8y8wAjSf4" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=ZNrYr03avVI:4RJ4Gljljlg:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=qj6IDK7rITs" border="0"></img></a> +</div><img src="http://feeds.feedburner.com/~r/MacRumors-Front/~4/ZNrYr03avVI" height="1" width="1"/> + Front Page + Fri, 15 Aug 2014 10:28:00 GMT + http://forums.macrumors.com/showthread.php?t=1765145 + http://www.macrumors.com/2014/08/15/new-lightning-cable-reversible-usb/ + Richard Padilla + + + diff --git a/spec/sample_data/feeds/feed01_valid_feed/feed_updated.xml b/spec/sample_data/feeds/feed01_valid_feed/feed_updated.xml new file mode 100644 index 000000000..5eb4a7943 --- /dev/null +++ b/spec/sample_data/feeds/feed01_valid_feed/feed_updated.xml @@ -0,0 +1,80 @@ + + + + + + MacRumors: Mac News and Rumors - Front Page + http://www.macrumors.com + the mac news you care about + en + Fri, 15 Aug 2014 18:34:02 GMT + Fri, 15 Aug 2014 18:34:02 GMT + 2 + hourly + 1 + 2014-08-15T18:35:13Z + + + + + Apple Adds Beats Music to App Store List of 'Apps Made by Apple' + http://www.macrumors.com/2014/08/15/beats-music-apps-by-apple/ + Following Apple's official acquisition of Beats Electronics and Beats Music, the Beats Music iOS app has been added to Apple's App Store listing of "Apps Made by Apple," giving the app high-profile placement to encourage downloads. <br/> <br/> This section of the iTunes Store, which also houses Apple-designed apps like <em>Pages,</em> <em>Numbers</em>, <em>Keynote</em>, <em>iPhoto</em>, and more, can be found in the Quick Links section of the iOS App Store and the desktop iTunes Store. <br/> <br/> <img src="http://cdn.macrumors.com/article-new/2014/08/appsmadebyapple.jpg" alt="appsmadebyapple" width="800" height="577" class="aligncenter size-full wp-image-419587" /> <br/> The addition of Beats Music to the Apps Made by Apple list follows its inclusion in the list of apps recommended to new iOS users, another move that will undoubtedly result in a high number of Beats Music app downloads. According to <em>Appshopper</em>, Beats Music's popularity has <a href="http://appshopper.com/music/beats-music">soared in recent weeks</a>. It is currently ranked fourth in music and number 23 in overall free apps. <br/> <br/> As of May, Beats Music had <a href="http://www.macrumors.com/2014/05/29/itunes-milestones-grow-beats-subscribers/">garnered 250,000 subscribers</a>, a number that has likely grown significantly with the app's prominent placement in the App Store and Apple's high-profile acquisition of the company. In contrast, Apple's iTunes Store has sold 35 billion songs and its existing streaming radio service, iTunes Radio, has more than 40 million listeners. <br/> <br/> Unlike many of the other apps listed in the Apps Made by Apple section, Beats Music is not free. While the app can be downloaded at no cost, it requires a monthly subscription priced at &#36;9.99. Unlike several competing streaming music services, Beats Music does not offer a free ad-supported listening. <br/> <br/> According to rumors, Apple has plans to keep the Beats Music service as a standalone entity, separate from its own iTunes and iTunes Radio offerings. <br/> <br/> <em>(Thanks, William!)</em><br/> <br/> <br/> <b>Recent Mac and iOS Blog Stories</b><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/apple-beats-spike-lee-documentary/">Beats Music Creates Documentary On Spike Lee&#039;s &#039;Do the Right Thing&#039;</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/stacksocial-maclovin-bundle/">StackSocial Offers 8 Mac Apps in &#039;MacLovin&#039; Bundle for &#36;39.99</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/apple-seeds-new-safari-betas-to-developers/">Apple Releases Safari 7.0.6 and Safari 6.1.6 With Security Fixes for Mavericks/Mountain Lion</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/mailbox-new-email-management-features/">Mailbox for iOS Gains New Email Management Features, Languages</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/os-x-mavericks-10-9-5-build-13f14/">Apple Seeds OS X Mavericks 10.9.5 Build 13F14 to Developers</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/amazon-local-register-card-reader/">Amazon Launches iOS Compatible &#039;Amazon Local Register&#039; Card Reader</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/12/skylanders-trap-team-controller-set/">Full &#039;Skylanders Trap Team&#039; Game Coming to iPad With Bluetooth Controller, Energy Portal</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/08/apple-iwatch-september-gruber/">Apple to Introduce iWatch in September Suggests Apple Journalist John Gruber [Updated]</a><br/><img width='1' height='1' src='http://rss.feedsportal.com/c/35070/f/648327/s/3d8be432/sc/28/mf.gif' border='0'/><br clear='all'/><br/><br/><a href="http://da.feedsportal.com/r/204366575142/u/49/f/648327/c/35070/s/3d8be432/sc/28/rc/1/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366575142/u/49/f/648327/c/35070/s/3d8be432/sc/28/rc/1/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366575142/u/49/f/648327/c/35070/s/3d8be432/sc/28/rc/2/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366575142/u/49/f/648327/c/35070/s/3d8be432/sc/28/rc/2/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366575142/u/49/f/648327/c/35070/s/3d8be432/sc/28/rc/3/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366575142/u/49/f/648327/c/35070/s/3d8be432/sc/28/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/204366575142/u/49/f/648327/c/35070/s/3d8be432/sc/28/a2.htm"><img src="http://da.feedsportal.com/r/204366575142/u/49/f/648327/c/35070/s/3d8be432/sc/28/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/204366575142/u/49/f/648327/c/35070/s/3d8be432/sc/28/a2t.img" border="0"/><div class="feedflare"> +<a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=vukRNCohdc0:PJtSiveaapc:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=vukRNCohdc0:PJtSiveaapc:6W8y8wAjSf4"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=6W8y8wAjSf4" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=vukRNCohdc0:PJtSiveaapc:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=qj6IDK7rITs" border="0"></img></a> +</div><img src="http://feeds.feedburner.com/~r/MacRumors-Front/~4/vukRNCohdc0" height="1" width="1"/> + Front Page + Fri, 15 Aug 2014 18:31:48 GMT + http://forums.macrumors.com/showthread.php?t=1765283 + http://www.macrumors.com/2014/08/15/beats-music-apps-by-apple/ + Juli Clover + + + Apple Working to Remedy Labor Violations Found at Quanta Factories + http://www.macrumors.com/2014/08/15/apple-labor-violations-quanta/ + The Fair Labor Association (FLA) today published <a href="http://www.fairlabor.org/2013-apple-quanta-shanghai-changshu">a new report </a>examining two factories operated by Apple-supplier Quanta Computer, finding several code violations related to working hours, recruitment policies, compensation, health and safety, and more in August of 2013 [<a href="http://www.fairlabor.org/sites/default/files/documents/reports/august-2014-apple-quanta-executive-summary_0.pdf">PDF</a>] (via <a href="http://techcrunch.com/2014/08/15/the-fair-labor-association-delivers-its-findings-on-two-apple-supplier-facilities/"><em>TechCrunch</em></a>). <br/> <br/> Factories examined included a Quanta facility in Shanghai and one in Changshu. Quanta is a long-time Apple partner that manufactures Apple's MacBook Air and much of the rest of the company's Mac lineup. <br/> <br/> Violations were found in both locations, with some of the more egregious issues including verbal abuse by supervisors, a hiring fee charged to workers by a broker or labor dispatch agent and long working hours. According to the report, 62 percent of workers in Changshu received no rest day for much of Q4 2012, working as many as 16 days in a row. <br/> <br/> <img src="http://cdn.macrumors.com/article-new/2014/08/supplierscore.jpg" alt="supplierscore" width="800" height="228" class="aligncenter size-full wp-image-419578" /><center><em>Overall score summary of management functions at Changshu</em></center> <br/> Many workers were also underpaid for sick leave and may have been uncompensated for up to an hour of work each day, based on clock in and clock out times. Some workers were forced into joining the All China Federation of Trade Unions, and there were several safety violations. <br/> <br/> Both of the factories fell short of the local requirements for indoor air quality, and neither had easy access to a shower/eyewash station in case of emergency. There was no active worker participation in the Employee Health and Safety committees, and flammable and toxic substances were stored improperly at Shanghai while chemicals at Changshu were not properly monitored. <br/> <br/> The Fair Labor Association provided a number of recommendations to improve conditions at the factory, and according to the report, Apple is using the recommendations to work with Quanta to fix each code violation. Apple released a statement on the FLA's Quanta inspection, stating that it has worked closely with Quanta to bring improvements to working conditions.<blockquote>Our suppliers must live up to the toughest standards in the industry if they want to keep doing business with Apple, which is the first and only technology company to be admitted to the Fair Labor Association. We are committed to providing safe and fair working conditions for everyone in our supply chain. <br/> <br/> Last year we conducted 451 comprehensive, in-person audits deep into our supply chain so we could uncover problems and work with our suppliers to fix them. We track and report the weekly working hours for more than 1 million workers, and our 18-month Apple Supplier EHS Academy training program is raising the bar for environment, health and safety management in the industry. <br/> <br/> The Quanta facilities inspected by the FLA last year were included in our 2014 Supplier Responsibility report, which we released in February. Our own experts have audited these sites 16 times, most recently last month. <br/> <br/> In the year since the FLA’s visit, we have worked closely with Quanta to drive meaningful improvements in areas identified by both the FLA and Apple. Apple conducted four follow-up inspections on top of the annual audits of both facilities, to ensure the needed corrections are in place. <br/> <br/> This year, through the end of July, Quanta has averaged 86 percent compliance with our 60-hour workweek. Excessive overtime is not in anyone’s best interest, and we will continue to work closely with Quanta and our other suppliers to prevent it.</blockquote>Apple initially signed up for factory assessments by the Fair Labor Association <a href="http://www.macrumors.com/2012/01/13/apple-partners-with-labor-group-to-monitor-workplace-conditions-at-suppliers-factories/">back in 2012</a>, following a rash of <a href="http://www.macrumors.com/2010/05/26/apple-independently-evaluating-foxconns-response-to-suicides-at-manufacturing-plant/">worker suicides</a> at Foxconn, the factory responsible for assembling many of Apple's mobile devices. The FLA has since helped to <a href="http://www.macrumors.com/2013/12/12/foxconn-and-apple-make-strides-towards-improving-work-hours-but-still-violate-chinese-limits/">improve working conditions</a> in several of Apple's factories, with Apple aiming to bring all workplace compliance standards in line with the FLA's guidelines. <br/> <br/> Apple also maintains a <a href="http://www.macrumors.com/2012/01/13/apple-releases-2012-supplier-responsibility-progress-report-and-supplier-list/">Supplier Responsibility team</a> that audits supply chain facilities to ensure compliance with Apple's strict code of conduct preventing underage labor and providing safe, comfortable living conditions for workers. An additional Supplier Responsiblity academic board also evaluates Apple's labor policies and practices and researches labor standards within the supply chain to create ethical working conditions wherever Apple products are produced.<br/> <br/> <br/> <b>Recent Mac and iOS Blog Stories</b><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/apple-beats-spike-lee-documentary/">Beats Music Creates Documentary On Spike Lee&#039;s &#039;Do the Right Thing&#039;</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/stacksocial-maclovin-bundle/">StackSocial Offers 8 Mac Apps in &#039;MacLovin&#039; Bundle for &#36;39.99</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/apple-seeds-new-safari-betas-to-developers/">Apple Releases Safari 7.0.6 and Safari 6.1.6 With Security Fixes for Mavericks/Mountain Lion</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/mailbox-new-email-management-features/">Mailbox for iOS Gains New Email Management Features, Languages</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/os-x-mavericks-10-9-5-build-13f14/">Apple Seeds OS X Mavericks 10.9.5 Build 13F14 to Developers</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/amazon-local-register-card-reader/">Amazon Launches iOS Compatible &#039;Amazon Local Register&#039; Card Reader</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/12/skylanders-trap-team-controller-set/">Full &#039;Skylanders Trap Team&#039; Game Coming to iPad With Bluetooth Controller, Energy Portal</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/08/apple-iwatch-september-gruber/">Apple to Introduce iWatch in September Suggests Apple Journalist John Gruber [Updated]</a><br/><img width='1' height='1' src='http://rss.feedsportal.com/c/35070/f/648327/s/3d8b2e60/sc/28/mf.gif' border='0'/><br clear='all'/><br/><br/><a href="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/rc/1/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/rc/1/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/rc/2/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/rc/2/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/rc/3/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/a2.htm"><img src="http://da.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/204366610557/u/49/f/648327/c/35070/s/3d8b2e60/sc/28/a2t.img" border="0"/><div class="feedflare"> +<a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=0wQ4Gbmj5Ac:XD-Tnjqu3Zw:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=0wQ4Gbmj5Ac:XD-Tnjqu3Zw:6W8y8wAjSf4"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=6W8y8wAjSf4" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=0wQ4Gbmj5Ac:XD-Tnjqu3Zw:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=qj6IDK7rITs" border="0"></img></a> +</div><img src="http://feeds.feedburner.com/~r/MacRumors-Front/~4/0wQ4Gbmj5Ac" height="1" width="1"/> + Front Page + Fri, 15 Aug 2014 17:36:37 GMT + http://forums.macrumors.com/showthread.php?t=1765267 + http://www.macrumors.com/2014/08/15/apple-labor-violations-quanta/ + Juli Clover + + + Apple Adds Five Vice Presidents, Including Two Women, to 'Apple Leadership' Press Page + http://www.macrumors.com/2014/08/15/apple-leadership-press-page-updates/ + Apple today updated its <a href="http://www.apple.com/pr/bios/">Apple Leadership</a> press page to add the bios of five vice presidents, including Paul Deneve, Lisa Jackson, Joel Podolny, Johny Srouji, and Denise Young Smith. <br/> <br/> The inclusion of several vice presidents on the executive team is a new move for the company, as the page previously only listed the company's lineup of senior vice presidents. <br/> <br/> <img src="http://cdn.macrumors.com/article-new/2014/08/applevps.jpg" alt="applevps" width="600" height="353" class="aligncenter size-full wp-image-419575" /> <br/> Since taking over as CEO, Tim Cook has aimed to share the spotlight with his employees, letting company leaders like Eddy Cue, Craig Federighi, and Jony Ive take part in <a href="http://www.macrumors.com/2013/09/25/jony-ive-and-craig-federighi-talk-collaboration-in-full-businessweek-interview/">press interviews</a> and lead Apple events. The addition of five new VPs to the press roster suggests Cook and the team at Apple are more open than ever, recognizing the efforts of an even larger number of key employees. <br/> <br/> Of the new executives joining the page, <a href="http://www.macrumors.com/2013/07/02/apple-to-hire-former-yves-saint-laurent-ceo-paul-deneve/">Paul Deneve</a> and <a href="http://www.macrumors.com/2013/05/28/apple-hires-former-epa-chief-lisa-jackson-to-oversee-environmental-efforts/">Lisa Jackson</a> are relatively new hires, joining Apple in 2013. Deneve oversees special projects, while Jackson has a public-facing job overseeing Apple's environmental efforts. Joel Podolny, dean of Apple University and Denise Young Smith, who oversees Apple's human resources, were <a href="http://www.macrumors.com/2014/02/11/podolny-apple-university/">promoted to their roles</a> early in 2014. The remaining VP, Johny Srouji, oversees hardware technologies, a role he has held for just over a year. <br/> <br/> The new additions to the executive press page also spotlight two additional women who hold important leadership roles at Apple, bringing the total number of females on the page to three, with Angela Ahrendts. Apple has faced criticism for its <a href="http://www.macrumors.com/2014/01/06/apple-updates-corporate-bylaws-on-diversity-following-criticism-over-lack-of-female-leaders/">lack of female leadership</a> in the past, which Cook has been aiming to remedy. According to diversity numbers released earlier this week, 72 percent of Apple employees in leadership roles are male. <br/> <br/> <small>Note: Due to the nature of the discussion regarding this topic, the discussion thread is located in our <a href="http://forums.macrumors.com/forumdisplay.php?f=47">Politics, Religion, Social Issues</a> forum. All forum members and site visitors are welcome to read and follow the thread, but posting is limited to forum members with at least 100 posts.</small><br/> <br/> <br/> <b>Recent Mac and iOS Blog Stories</b><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/apple-beats-spike-lee-documentary/">Beats Music Creates Documentary On Spike Lee&#039;s &#039;Do the Right Thing&#039;</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/stacksocial-maclovin-bundle/">StackSocial Offers 8 Mac Apps in &#039;MacLovin&#039; Bundle for &#36;39.99</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/apple-seeds-new-safari-betas-to-developers/">Apple Releases Safari 7.0.6 and Safari 6.1.6 With Security Fixes for Mavericks/Mountain Lion</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/mailbox-new-email-management-features/">Mailbox for iOS Gains New Email Management Features, Languages</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/os-x-mavericks-10-9-5-build-13f14/">Apple Seeds OS X Mavericks 10.9.5 Build 13F14 to Developers</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/amazon-local-register-card-reader/">Amazon Launches iOS Compatible &#039;Amazon Local Register&#039; Card Reader</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/12/skylanders-trap-team-controller-set/">Full &#039;Skylanders Trap Team&#039; Game Coming to iPad With Bluetooth Controller, Energy Portal</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/08/apple-iwatch-september-gruber/">Apple to Introduce iWatch in September Suggests Apple Journalist John Gruber [Updated]</a><br/><img width='1' height='1' src='http://rss.feedsportal.com/c/35070/f/648327/s/3d8b03fc/sc/21/mf.gif' border='0'/><br clear='all'/><br/><br/><a href="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/rc/1/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/rc/1/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/rc/2/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/rc/2/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/rc/3/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/a2.htm"><img src="http://da.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/204366587107/u/49/f/648327/c/35070/s/3d8b03fc/sc/21/a2t.img" border="0"/><div class="feedflare"> +<a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=pGJ2BCCYpIg:2bXK9R8yazc:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=pGJ2BCCYpIg:2bXK9R8yazc:6W8y8wAjSf4"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=6W8y8wAjSf4" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=pGJ2BCCYpIg:2bXK9R8yazc:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=qj6IDK7rITs" border="0"></img></a> +</div><img src="http://feeds.feedburner.com/~r/MacRumors-Front/~4/pGJ2BCCYpIg" height="1" width="1"/> + Front Page + Fri, 15 Aug 2014 17:11:42 GMT + http://forums.macrumors.com/showthread.php?t=1765257 + http://www.macrumors.com/2014/08/15/apple-leadership-press-page-updates/ + Juli Clover + + + Earphones Filled With Health and Fitness Sensors Ready to Flood the Wearables Market + http://www.macrumors.com/2014/08/15/health-fitness-sensor-earphones/ + As Apple moves closer to launching its rumored health and fitness oriented iWatch, there has been significant focus on the types of sensors Apple might be looking to deploy. Among the intriguing options have been sensor-equipped earphones, which Apple has filed patent applications on in the past and which saw renewed attention following the posting of an <a href="http://www.macrumors.com/2014/05/01/earpods-biometric-sensors/">unfounded rumor</a> about EarPods with embedded biometric sensors earlier this year. <br/> <br/> Earphones with health and fitness sensors do continue to be a topic of interest, and other manufacturers are preparing to bring their own products to the wearables market later this year. One biometric headphone attracting attention is <a href="http://newsroom.intel.com/community/intel_newsroom/blog/2014/08/14/intel-and-sms-audio-to-supercharge-fitness-wearables">a new offering</a> from Intel and SMS Audio, which has musician 50 Cent as a co-founder (via <a href="http://recode.net/2014/08/14/gimme-a-beat-intel-50-cent-pair-up-on-heart-rate-headphones/"><em>Re/Code</em></a>). <br/> <br/> <img src="http://cdn.macrumors.com/article-new/2014/08/intel-ear-buds.jpeg" alt="intel-ear-buds" width="640" height="480" class="aligncenter size-full wp-image-419561" /> <br/> The SMS Audio BioSport In-Ear Headphones include heart rate monitoring powered by Intel technology and feature SMS Audio's high-quality sound, comfort and fashion. The ear buds will use a combination of sensors, including an optical light sensor, to measure both resting and active heart rates. Powered by the smartphone's audio jack, the headphones will share their data with a variety of third-party apps, with RunKeeper integration available at launch. <br/> <br/> Intel and SMS Audio may be among the biggest names entering the sensor headphone market, but they are not alone. Earlier this year, LG <a href="http://www.lg.com/us/fitness-activity-trackers/lg-FR74-heart-rate-monitor">released</a> its own Bluetooth-connected, heart-rate-measuring earphones with <a href="http://recode.net/2014/06/16/lgs-heart-rate-earphones-might-not-win-your-heart/">mixed reviews</a>. Hitting the market later this year are wireless <a href="https://www.kickstarter.com/projects/freewavz/freewavz-smart-earphones-with-built-in-fitness-mon">Smart Earphones from FreeWavz</a>, which monitors heart rate and other key fitness metrics. The Kickstarter-backed product reached its funding goal earlier this month and has a target ship date of October 2014. <br/> <br/> Beyond the ultimately false rumor of EarPods with biometric sensors and Apple's patent applications dating back a number of years, the company also <a href="http://www.macrumors.com/2014/05/01/apple-hires-another-wearables-expert/">recently hired</a> former MIT researcher Eric Winokur as a Sensing Hardware engineer. Winokur is known for his work on wearable medical devices, including ear-worn sensors for cardiovascular monitoring. It is not known, however, whether Winokur is continuing his work on ear-worn sensors at Apple or if he is contributing more broadly to the <a href="http://www.macrumors.com/2014/02/13/apple-biometrics-team-lamego/">biometrics team</a> working on the iWatch. <br/> <br/> <br/> <b>Recent Mac and iOS Blog Stories</b><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/apple-beats-spike-lee-documentary/">Beats Music Creates Documentary On Spike Lee&#039;s &#039;Do the Right Thing&#039;</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/stacksocial-maclovin-bundle/">StackSocial Offers 8 Mac Apps in &#039;MacLovin&#039; Bundle for &#36;39.99</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/apple-seeds-new-safari-betas-to-developers/">Apple Releases Safari 7.0.6 and Safari 6.1.6 With Security Fixes for Mavericks/Mountain Lion</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/mailbox-new-email-management-features/">Mailbox for iOS Gains New Email Management Features, Languages</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/os-x-mavericks-10-9-5-build-13f14/">Apple Seeds OS X Mavericks 10.9.5 Build 13F14 to Developers</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/amazon-local-register-card-reader/">Amazon Launches iOS Compatible &#039;Amazon Local Register&#039; Card Reader</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/12/skylanders-trap-team-controller-set/">Full &#039;Skylanders Trap Team&#039; Game Coming to iPad With Bluetooth Controller, Energy Portal</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/08/apple-iwatch-september-gruber/">Apple to Introduce iWatch in September Suggests Apple Journalist John Gruber [Updated]</a><br/><img width='1' height='1' src='http://rss.feedsportal.com/c/35070/f/648327/s/3d8a861d/sc/28/mf.gif' border='0'/><br clear='all'/><br/><br/><a href="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/rc/1/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/rc/1/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/rc/2/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/rc/2/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/rc/3/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/a2.htm"><img src="http://da.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/204366606844/u/49/f/648327/c/35070/s/3d8a861d/sc/28/a2t.img" border="0"/><div class="feedflare"> +<a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=3SJMnGU-uh0:k2Wk9HJTvCM:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=3SJMnGU-uh0:k2Wk9HJTvCM:6W8y8wAjSf4"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=6W8y8wAjSf4" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=3SJMnGU-uh0:k2Wk9HJTvCM:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=qj6IDK7rITs" border="0"></img></a> +</div><img src="http://feeds.feedburner.com/~r/MacRumors-Front/~4/3SJMnGU-uh0" height="1" width="1"/> + Front Page + Fri, 15 Aug 2014 15:08:17 GMT + http://forums.macrumors.com/showthread.php?t=1765221 + http://www.macrumors.com/2014/08/15/health-fitness-sensor-earphones/ + Kelly Hodgkins + + + Apple Now Using China Telecom as Data Center Provider in China + http://www.macrumors.com/2014/08/15/apple-china-telecom-data-center/ + <img src="http://cdn.macrumors.com/article-new/2014/08/china_telecom_logo-250x250.png" alt="china_telecom_logo" width="250" height="250" class="alignright size-medium wp-image-419553" /> Apple has officially added China Telecom as a data center provider in China, <a href="http://blogs.wsj.com/digits/2014/08/15/apple-adds-china-telecom-as-data-center-provider/?mod=rss_Technology">reports</a> <em>The Wall Street Journal</em>. The move comes after 15 months of "stringent tests and evaluation" by the Fuzhou city government, as Apple states that all data stored on the servers is encrypted. According to <em><a href="http://www.reuters.com/article/2014/08/15/us-apple-data-china-idUSKBN0GF0N720140815?feedType=RSS&#38;feedName=technologyNews">Reuters</a></em>, Apple says the new data center will help improve the speed and reliability of iCloud and the iTunes Store in the region. <blockquote>"Apple takes user security and privacy very seriously. We have added China Telecom to our list of data center providers to increase bandwidth and improve performance for our customers in mainland China," it said.</blockquote> The move could also help ease tensions between Apple and China as of late, as the country recently <a href="http://www.macrumors.com/2014/07/11/chinese-media-iphone-location-tracking-security/">deemed</a> iOS' location tracking services a "national security concern." Apple <a href="http://www.macrumors.com/2014/07/12/apple-responds-china-ios-location-tracking/">responded</a> to those claims reiterating its commitment to privacy and stating that its Location Services exist to aide navigation features. <br/> <br/> China has become an important market for Apple, as the company has looked to improve its presence in the country as of late. Late last year, the company <a href="http://www.macrumors.com/2013/12/04/apple-finally-signs-deal-with-china-mobile-for-december-iphone-launch/">started</a> selling the iPhone on China Mobile, the country's biggest carrier, and opened more retail stores throughout the region. CEO Tim Cook has also made a number of <a href="http://www.macrumors.com/2012/03/28/apple-ceo-tim-cook-meets-with-beijing-mayor-chinese-vice-premier-during-visit/">visits</a> to China, meeting with Bejing's mayor and the Chinese Vice Premier to discuss opportunities. <br/> <br/> <br/> <b>Recent Mac and iOS Blog Stories</b><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/apple-beats-spike-lee-documentary/">Beats Music Creates Documentary On Spike Lee&#039;s &#039;Do the Right Thing&#039;</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/14/stacksocial-maclovin-bundle/">StackSocial Offers 8 Mac Apps in &#039;MacLovin&#039; Bundle for &#36;39.99</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/apple-seeds-new-safari-betas-to-developers/">Apple Releases Safari 7.0.6 and Safari 6.1.6 With Security Fixes for Mavericks/Mountain Lion</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/mailbox-new-email-management-features/">Mailbox for iOS Gains New Email Management Features, Languages</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/os-x-mavericks-10-9-5-build-13f14/">Apple Seeds OS X Mavericks 10.9.5 Build 13F14 to Developers</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/13/amazon-local-register-card-reader/">Amazon Launches iOS Compatible &#039;Amazon Local Register&#039; Card Reader</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/12/skylanders-trap-team-controller-set/">Full &#039;Skylanders Trap Team&#039; Game Coming to iPad With Bluetooth Controller, Energy Portal</a><br/> &#8226; <a href="http://www.macrumors.com/2014/08/08/apple-iwatch-september-gruber/">Apple to Introduce iWatch in September Suggests Apple Journalist John Gruber [Updated]</a><br/><img width='1' height='1' src='http://rss.feedsportal.com/c/35070/f/648327/s/3d88e7a5/sc/28/mf.gif' border='0'/><br clear='all'/><br/><br/><a href="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/rc/1/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/rc/1/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/rc/2/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/rc/2/rc.img" border="0"/></a><br/><a href="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/rc/3/rc.htm" rel="nofollow"><img src="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/rc/3/rc.img" border="0"/></a><br/><br/><a href="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/a2.htm"><img src="http://da.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/a2.img" border="0"/></a><img width="1" height="1" src="http://pi.feedsportal.com/r/204366599485/u/49/f/648327/c/35070/s/3d88e7a5/sc/28/a2t.img" border="0"/><div class="feedflare"> +<a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=5orxG6suAvU:4cJ4URr9D-c:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=5orxG6suAvU:4cJ4URr9D-c:6W8y8wAjSf4"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=6W8y8wAjSf4" border="0"></img></a> <a href="http://feeds.macrumors.com/~ff/MacRumors-Front?a=5orxG6suAvU:4cJ4URr9D-c:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/MacRumors-Front?d=qj6IDK7rITs" border="0"></img></a> +</div><img src="http://feeds.feedburner.com/~r/MacRumors-Front/~4/5orxG6suAvU" height="1" width="1"/> + Front Page + Fri, 15 Aug 2014 11:07:04 GMT + http://forums.macrumors.com/showthread.php?t=1765148 + http://www.macrumors.com/2014/08/15/apple-china-telecom-data-center/ + Richard Padilla + + + diff --git a/spec/support/feed_server.rb b/spec/support/feed_server.rb new file mode 100644 index 000000000..e6abdbc5a --- /dev/null +++ b/spec/support/feed_server.rb @@ -0,0 +1,15 @@ +class FeedServer + attr_writer :response + + def initialize + @server = Capybara::Server.new(method(:response)).boot + end + + def response(env) + [200, {}, [@response]] + end + + def url + "http://#{@server.host}:#{@server.port}" + end +end From b072bbb0bd3b46c32beb461645ed07e9fae353da Mon Sep 17 00:00:00 2001 From: Pascal Widdershoven Date: Fri, 15 Aug 2014 21:08:41 +0200 Subject: [PATCH 0047/1107] Add integration test for feed with invalid pub dates --- spec/integration/feed_importing_spec.rb | 21 ++ .../feed02_invalid_published_dates/feed.xml | 239 ++++++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 spec/sample_data/feeds/feed02_invalid_published_dates/feed.xml diff --git a/spec/integration/feed_importing_spec.rb b/spec/integration/feed_importing_spec.rb index ce44b4f6c..7d1f48a2c 100644 --- a/spec/integration/feed_importing_spec.rb +++ b/spec/integration/feed_importing_spec.rb @@ -1,4 +1,5 @@ require "spec_helper" +require "time" require "support/active_record" require "support/feed_server" require "capybara" @@ -48,6 +49,26 @@ end end end + + describe "Feed with incorrect pubdates" do + context "has been fetched before" do + it "imports all new stories" do + # This spec describes a scenario where the feed is reporting incorrect + # published dates for stories. + # The feed in question is feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots. + # When an article is published the published date is always set to 00:00 of + # the day the article was published. + # This specs shows that with the current behaviour (08-15-2014) Stringer + # will not detect this article, if the last time this feed was fetched is + # after 00:00 the day the article was published. + + feed.last_fetched = Time.parse '2014-08-12T00:01:00Z' + @server.response = sample_data('feeds/feed02_invalid_published_dates/feed.xml') + + expect { fetch_feed(feed) }.to change{ feed.stories.count }.by(1) + end + end + end end def sample_data(path) diff --git a/spec/sample_data/feeds/feed02_invalid_published_dates/feed.xml b/spec/sample_data/feeds/feed02_invalid_published_dates/feed.xml new file mode 100644 index 000000000..fe0ef1828 --- /dev/null +++ b/spec/sample_data/feeds/feed02_invalid_published_dates/feed.xml @@ -0,0 +1,239 @@ + + + Giant Robots Smashing Into Other Giant Robots + Written by thoughtbot + http://robots.thoughtbot.com + + + 2014-08-12T00:00:00Z + + thoughtbot + + GiantRobotsSmashingIntoOtherGiantRobotshttp://feedburner.google.comThis is an XML content feed. It is intended to be viewed in a newsreader or syndicated to another site, subject to copyright and fair use. + Buttons with Hold Events in Angular.js + + + Sean Griffin + + http://robots.thoughtbot.com/buttons-with-hold-events-in-angularjs + 2014-08-12T00:00:00Z + 2014-08-12T14:08:35+00:00 + <p>Creating an interaction with a simple button in Angular only requires adding the +<a href="https://docs.angularjs.org/api/ng/directive/ngClick">ngClick</a> directive. However, sometimes an on click style interaction isn&rsquo;t +sufficient. Let&rsquo;s take a look at how we can have a button which performs an +action as long as it&rsquo;s pressed.</p> + +<p>For the example, we&rsquo;ll use two buttons which can be used to zoom a camera in and +out. We want the camera to continue zooming, until the button is released. The +final effect will work like this:</p> + +<p><a href="https://www.martialcodex.com"><img alt="Zooming in Martial Codex" src="http://images.thoughtbot.com/martial-codex/zoom-demo.gif" /></a></p> + +<p>Our template might look something like this:</p> + +<pre><code class="html">&lt;a href while-pressed=&quot;zoomOut()&quot;&gt; + &lt;i class=&quot;fa fa-minus&quot;&gt;&lt;/i&gt; +&lt;/a&gt; +&lt;a href while-pressed=&quot;zoomIn()&quot;&gt; + &lt;i class=&quot;fa fa-plus&quot;&gt;&lt;/i&gt; +&lt;/a&gt; +</code></pre> + +<p>We&rsquo;re making a subtle assumption with this interface. By adding the parenthesis, +we imply that <code>whilePressed</code> will behave similarly to <code>ngClick</code>. The given value +is an expression that will get evaluated continuously when the button is +pressed, rather than us handing it a function object for it to call. In +practice, we can use the <code>&#39;&amp;&#39;</code> style of arguments in our directive to capture +the expression. You can find more information about the different styles of +scopes <a href="https://docs.angularjs.org/api/ng/service/$compile#-scope-">here</a>.</p> + +<pre><code class="coffeescript">whilePressed = -&gt; + restrict: &quot;A&quot; + + scope: + whilePressed: &#39;&amp;&#39; +</code></pre> + +<h2>Binding the Events</h2> + +<p>When defining more complex interactions such as this one, Angular&rsquo;s built-in +directives won&rsquo;t give us the control we need. Instead, we&rsquo;ll fall back to manual +event binding on the element. For clarity, I tend prefer to separate the +callback function from the event bindings. Since we&rsquo;re manipulating the DOM, our +code will go into a <a href="https://docs.angularjs.org/guide/directive#creating-a-directive-that-manipulates-the-dom">link function</a>. Our initial link function will look +like this:</p> + +<pre><code class="coffeescript">link: (scope, elem, attrs) -&gt; + action = scope.whilePressed + + bindWhilePressed = -&gt; + elem.on(&quot;mousedown&quot;, beginAction) + + beginAction = (e) -&gt; + e.preventDefault() + # Do stuff + + bindWhilePressed() +</code></pre> + +<p>Inside of our action we&rsquo;ll need to do two things:</p> + +<ol> +<li>Start running the action</li> +<li>Bind to <code>mouseup</code> to stop running the action.</li> +</ol> + +<p>For running the action, we&rsquo;ll use Angular&rsquo;s <a href="https://docs.angularjs.org/api/ngMock/service/$interval"><code>$interval</code></a> service. +<code>$interval</code> is a wrapper around JavaScript&rsquo;s <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window.setInterval"><code>setInterval</code></a>, but +gives us a promise interface, better testability, and hooks into Angular&rsquo;s +digest cycle.</p> + +<p>In addition to running the action continuously, we&rsquo;ll also want to run it +immediately to avoid a delay. We&rsquo;ll run the action every 15 milliseconds, as +this will roughly translate to once per browser frame.</p> + +<pre><code class="patch">+TICK_LENGTH = 15 ++ +-whilePressed = -&gt; ++whilePressed = ($interval) -&gt; + restrict: &quot;A&quot; + + link: + action = scope.whilePressed + +@@ -23,7 +24,7 @@ + beginAction = (e) -&gt; + e.preventDefault() ++ action() ++ $interval(action, TICK_LENGTH) ++ bindEndAction() +</code></pre> + +<p>In our <code>beginAction</code> function, we call <code>bindEndAction</code> to set up the events to +stop running the event. We know that we&rsquo;ll at least want to bind to <code>mouseup</code> on +our button, but we have to decide how to handle users who move the mouse off of +the button before releasing it. We can handle this by listening for <code>mouseleave</code> +on the element, in addition to <code>mouseup</code>.</p> + +<pre><code class="coffeescript">bindEndAction = -&gt; + elem.on(&#39;mouseup&#39;, endAction) + elem.on(&#39;mouseleave&#39;, endAction) +</code></pre> + +<p>In our <code>endAction</code> function, we&rsquo;ll want to cancel the <code>$interval</code> for our +action, and unbind the event listeners for <code>mouseup</code> and <code>mouseleave</code>.</p> + +<pre><code class="coffeescript">unbindEndAction = -&gt; + elem.off(&#39;mouseup&#39;, endAction) + elem.off(&#39;mouseleave&#39;, endAction) + +endAction = -&gt; + $interval.cancel(intervalPromise) + unbindEndAction() +</code></pre> + +<p>We&rsquo;ll also need to store the promise that <code>$interval</code> returned so that we can +cancel it when the mouse is released.</p> + +<pre><code class="patch"> whilePressed = ($parse, $interval) -&gt; + link: (scope, elem, attrs) -&gt; + action = scope.whilePressed ++ intervalPromise = null + + bindWhilePressed = -&gt; + elem.on(&#39;mousedown&#39;, beginAction) +@@ -23,7 +24,7 @@ + beginAction = (e) -&gt; + e.preventDefault() + action() +- $interval(action, TICK_LENGTH) ++ intervalPromise = $interval(action, TICK_LENGTH) + bindEndAction() +</code></pre> + +<h2>Cleaning Up</h2> + +<p>Generally I consider it a smell to have an isolated scope on any directive that +isn&rsquo;t an element. Each DOM element can only have one isolated scope, and +attribute directives are generally meant to be composed. So let&rsquo;s replace our +scope with a manual use of <a href="https://docs.angularjs.org/api/ng/service/$parse"><code>$parse</code></a> instead.</p> + +<p><code>$parse</code> takes in an expression, and will return a function that can be called +with a scope and an optional hash of local variables. This means we can&rsquo;t call +<code>action</code> directly anymore, and instead need a wrapper function which will pass in +the scope for us.</p> + +<pre><code class="patch">-whilePressed = ($interval) -&gt; +- scope: +- whilePressed: &quot;&amp;&quot; +- ++whilePressed = ($parse, $interval) -&gt; + link: (scope, elem, attrs) -&gt; +- action = scope.whilePressed ++ action = $parse(attrs.whilePressed) + intervalPromise = null + + bindWhilePressed = -&gt; +@@ -26,14 +23,17 @@ whilePressed = ($interval) -&gt; + + beginAction = (e) -&gt; + e.preventDefault() +- action() +- intervalPromise = $interval(action, TICK_LENGTH) ++ tickAction() ++ intervalPromise = $interval(tickAction, TICK_LENGTH) + bindEndAction() + + endAction = -&gt; + $interval.cancel(intervalPromise) + unbindEndAction() + ++ tickAction = -&gt; ++ action(scope) +</code></pre> + +<p>And that&rsquo;s it. Our end result is a nicely decoupled Angular UI component that +can easily be reused across applications. The final code looks like this.</p> + +<pre><code class="coffeescript">TICK_LENGTH = 15 + +whilePressed = ($parse, $interval) -&gt; + restrict: &quot;A&quot; + + link: (scope, elem, attrs) -&gt; + action = $parse(attrs.whilePressed) + intervalPromise = null + + bindWhilePressed = -&gt; + elem.on(&#39;mousedown&#39;, beginAction) + + bindEndAction = -&gt; + elem.on(&#39;mouseup&#39;, endAction) + elem.on(&#39;mouseleave&#39;, endAction) + + unbindEndAction = -&gt; + elem.off(&#39;mouseup&#39;, endAction) + elem.off(&#39;mouseleave&#39;, endAction) + + beginAction = (e) -&gt; + e.preventDefault() + tickAction() + intervalPromise = $interval(tickAction, TICK_LENGTH) + bindEndAction() + + endAction = -&gt; + $interval.cancel(intervalPromise) + unbindEndAction() + + tickAction = -&gt; + action(scope) + + bindWhilePressed() +</code></pre> + + <p>Creating an interaction with a simple button in Angular only requires adding the +<a href="https://docs.angularjs.org/api/ng/directive/ngClick">ngClick</a> directive. However, sometimes an on click style interaction isn’t +sufficient. Let’s take a look at how we can have a button which performs an +action as long as...</p> + http://robots.thoughtbot.com/buttons-with-hold-events-in-angularjs + From 7059a6dd8e2ab17590506f8e6cfc59c30f65dca1 Mon Sep 17 00:00:00 2001 From: Pascal Widdershoven Date: Fri, 15 Aug 2014 22:07:59 +0200 Subject: [PATCH 0048/1107] Change feed importer to use story ids to determine if a story is new This should make feed importing more robust. Pub dates of feeds and articles can not always be trusted (see https://github.com/swanson/stringer/issues/261). Using this mechanism we should always reliably be able to tell which stories are new, regardless of pub dates being correctly reported or not. --- app/commands/feeds/find_new_stories.rb | 12 +++--- app/repositories/story_repository.rb | 4 ++ app/tasks/fetch_feed.rb | 2 +- spec/commands/find_new_stories_spec.rb | 53 ++++++++++++-------------- spec/tasks/fetch_feed_spec.rb | 5 ++- 5 files changed, 39 insertions(+), 37 deletions(-) diff --git a/app/commands/feeds/find_new_stories.rb b/app/commands/feeds/find_new_stories.rb index 8db937080..b217102e0 100644 --- a/app/commands/feeds/find_new_stories.rb +++ b/app/commands/feeds/find_new_stories.rb @@ -1,20 +1,20 @@ +require_relative "../../repositories/story_repository" + class FindNewStories - def initialize(raw_feed, last_fetched, latest_entry_id = nil) + def initialize(raw_feed, feed_id, last_fetched, latest_entry_id = nil) @raw_feed = raw_feed + @feed_id = feed_id @last_fetched = last_fetched @latest_entry_id = latest_entry_id end def new_stories - return [] if @raw_feed.last_modified && - @raw_feed.last_modified < @last_fetched - stories = [] + @raw_feed.entries.each do |story| break if @latest_entry_id && story.id == @latest_entry_id - stories << story unless story.published && - story.published < @last_fetched + stories << story unless StoryRepository.exists?(story.id, @feed_id) end stories diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index 04f0cbf8a..4ef9f2c4d 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -39,6 +39,10 @@ def self.save(story) story.save end + def self.exists?(id, feed_id) + Story.exists?(entry_id: id, feed_id: feed_id) + end + def self.unread Story.where(is_read: false).order("published desc").includes(:feed) end diff --git a/app/tasks/fetch_feed.rb b/app/tasks/fetch_feed.rb index 4c86c60e7..c1c9d2810 100644 --- a/app/tasks/fetch_feed.rb +++ b/app/tasks/fetch_feed.rb @@ -46,7 +46,7 @@ def fetch private def new_entries_from(raw_feed) - finder = FindNewStories.new(raw_feed, @feed.last_fetched, latest_entry_id) + finder = FindNewStories.new(raw_feed, @feed.id, @feed.last_fetched, latest_entry_id) finder.new_stories end diff --git a/spec/commands/find_new_stories_spec.rb b/spec/commands/find_new_stories_spec.rb index 40d0d8d30..cdf7e3478 100644 --- a/spec/commands/find_new_stories_spec.rb +++ b/spec/commands/find_new_stories_spec.rb @@ -1,49 +1,46 @@ require "spec_helper" +app_require "repositories/story_repository" app_require "commands/feeds/find_new_stories" describe FindNewStories do describe "#new_stories" do - context "the feed has not been updated" do + context "the feed contains no new stories" do + before do + StoryRepository.stub(:exists?).and_return(true) + end + it "should find zero new stories" do - feed = double(last_modified: Time.new(2013, 1, 1)) + story1 = double(id: "story1") + story2 = double(id: "story2") + feed = double(entries: [story1, story2]) - result = FindNewStories.new(feed, Time.new(2013, 1, 2)).new_stories + result = FindNewStories.new(feed, 1, Time.new(2013, 1, 2)).new_stories result.should be_empty end end - context "the feed has been updated" do - it "should return stories that are new based on published date" do - new_story = double(published: Time.new(2013, 1, 5)) - old_story = double(published: Time.new(2013, 1, 1)) - feed = double(last_modified: Time.new(2013, 1, 5), entries: [new_story, old_story]) - - result = FindNewStories.new(feed, Time.new(2013, 1, 3)).new_stories - result.should eq [new_story] - end - end + context "the feed contains new stories" do + it "should return stories that are not found in the database" do + story1 = double(id: "story1") + story2 = double(id: "story2") + feed = double(entries: [story1, story2]) - context "the feed does not report last_modified" do - it "should check all stories and compare published time" do - new_story = double(published: Time.new(2013, 1, 5)) - old_story = double(published: Time.new(2013, 1, 1)) - feed = double(last_modified: nil, entries: [new_story, old_story]) + StoryRepository.stub(:exists?).with("story1", 1).and_return(true) + StoryRepository.stub(:exists?).with("story2", 1).and_return(false) - result = FindNewStories.new(feed, Time.new(2013, 1, 3)).new_stories - result.should eq [new_story] + result = FindNewStories.new(feed, 1, Time.new(2013, 1, 2)).new_stories + result.should eq [story2] end end - context "the feed has no timekeeping" do - it "should scan until matching the last story id" do - new_story = double(published: nil, id: "new-story") - old_story = double(published: nil, id: "old-story") - feed = double(last_modified: nil, entries: [new_story, old_story]) + it "should scan until matching the last story id" do + new_story = double(published: nil, id: "new-story") + old_story = double(published: nil, id: "old-story") + feed = double(last_modified: nil, entries: [new_story, old_story]) - result = FindNewStories.new(feed, Time.new(2013, 1, 3), "old-story").new_stories - result.should eq [new_story] - end + result = FindNewStories.new(feed, 1, Time.new(2013, 1, 3), "old-story").new_stories + result.should eq [new_story] end end end diff --git a/spec/tasks/fetch_feed_spec.rb b/spec/tasks/fetch_feed_spec.rb index 86a8e76a1..ba061719a 100644 --- a/spec/tasks/fetch_feed_spec.rb +++ b/spec/tasks/fetch_feed_spec.rb @@ -4,7 +4,8 @@ describe FetchFeed do describe "#fetch" do let(:daring_fireball) do - double(url: "http://daringfireball.com/feed", + double(id: 1, + url: "http://daringfireball.com/feed", last_fetched: Time.new(2013,1,1), stories: []) end @@ -65,7 +66,7 @@ context "feed status" do it "sets the status to green if things are all good" do - fake_feed = double(last_modified: Time.new(2012, 12, 31)) + fake_feed = double(last_modified: Time.new(2012, 12, 31), entries: []) parser = double(fetch_and_parse: fake_feed) FeedRepository.should_receive(:set_status) From 078745b4ba4cb8e07bf945bb877612317ade98d6 Mon Sep 17 00:00:00 2001 From: Pascal Widdershoven Date: Sat, 16 Aug 2014 15:31:43 +0200 Subject: [PATCH 0049/1107] Adhere to project code style (string double quotes) --- spec/integration/feed_importing_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/integration/feed_importing_spec.rb b/spec/integration/feed_importing_spec.rb index 7d1f48a2c..42639bb59 100644 --- a/spec/integration/feed_importing_spec.rb +++ b/spec/integration/feed_importing_spec.rb @@ -23,27 +23,27 @@ describe "Valid feed" do describe "Importing for the first time" do it "imports all entries" do - @server.response = sample_data('feeds/feed01_valid_feed/feed.xml') + @server.response = sample_data("feeds/feed01_valid_feed/feed.xml") expect { fetch_feed(feed) }.to change{ feed.stories.count }.to(5) end end describe "Importing for the second time" do before(:each) do - @server.response = sample_data('feeds/feed01_valid_feed/feed.xml') + @server.response = sample_data("feeds/feed01_valid_feed/feed.xml") fetch_feed(feed) end context "no new entries" do it "does not create new stories" do - @server.response = sample_data('feeds/feed01_valid_feed/feed.xml') + @server.response = sample_data("feeds/feed01_valid_feed/feed.xml") expect { fetch_feed(feed) }.to_not change{ feed.stories.count } end end context "new entries" do it "creates new stories" do - @server.response = sample_data('feeds/feed01_valid_feed/feed_updated.xml') + @server.response = sample_data("feeds/feed01_valid_feed/feed_updated.xml") expect { fetch_feed(feed) }.to change{ feed.stories.count }.by(1).to(6) end end @@ -62,8 +62,8 @@ # will not detect this article, if the last time this feed was fetched is # after 00:00 the day the article was published. - feed.last_fetched = Time.parse '2014-08-12T00:01:00Z' - @server.response = sample_data('feeds/feed02_invalid_published_dates/feed.xml') + feed.last_fetched = Time.parse("2014-08-12T00:01:00Z") + @server.response = sample_data("feeds/feed02_invalid_published_dates/feed.xml") expect { fetch_feed(feed) }.to change{ feed.stories.count }.by(1) end From 12e5184a0f80570c46e13946acef4e214f2e95ab Mon Sep 17 00:00:00 2001 From: Pascal Widdershoven Date: Sat, 16 Aug 2014 20:24:11 +0200 Subject: [PATCH 0050/1107] Support feeds with stories with scheme-less urls Example is https://blog.golang.org/feed.atom. Urls in this feed are referenced to as scheme-less urls (//blog.golang.org/context). This commit adds normalization of story urls using the feed url. If a story url doesn't have a scheme it will use the scheme of feed's url. --- app/repositories/story_repository.rb | 13 ++++++++ spec/repositories/story_repository_spec.rb | 36 ++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index 04f0cbf8a..ad6ee6a55 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -3,6 +3,8 @@ class StoryRepository def self.add(entry, feed) + entry.url = normalize_url(entry.url, feed.url) + Story.create(feed: feed, title: entry.title, permalink: entry.url, @@ -109,6 +111,17 @@ def self.expand_absolute_urls(content, base_url) doc.to_html end + def self.normalize_url(url, base_url) + uri = URI.parse(url) + + unless uri.scheme + base_uri = URI.parse(base_url) + uri.scheme = base_uri.scheme || 'http' + end + + uri.to_s + end + def self.samples [ SampleStory.new("Darin' Fireballs", "Why you should trade your firstborn for a Retina iPad"), diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 4c04ed06b..dd74a68be 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -3,6 +3,20 @@ app_require "repositories/story_repository" describe StoryRepository do + describe '.add' do + let(:feed) { double(url: 'http://blog.golang.org/feed.atom') } + before do + Story.stub(:create) + end + + it 'normalizes story urls' do + entry = double(url: '//blog.golang.org/context', content: '').as_null_object + StoryRepository.should receive(:normalize_url).with(entry.url, feed.url) + + StoryRepository.add(entry, feed) + end + end + describe ".expand_absolute_urls" do it "preserves existing absolute urls" do content = 'bar' @@ -98,4 +112,26 @@ end end end + + describe ".normalize_url" do + it "resolves scheme-less urls" do + %w{http https}.each do |scheme| + feed_url = "#{scheme}://blog.golang.org/feed.atom" + + url = StoryRepository.normalize_url("//blog.golang.org/context", feed_url) + url.should eq "#{scheme}://blog.golang.org/context" + end + end + + it "leaves urls with a scheme intact" do + input = 'http://blog.golang.org/context' + normalized_url = StoryRepository.normalize_url(input, 'http://blog.golang.org/feed.atom') + normalized_url.should eq(input) + end + + it "falls back to http if the base_url is also sheme less" do + url = StoryRepository.normalize_url("//blog.golang.org/context", "//blog.golang.org/feed.atom") + url.should eq 'http://blog.golang.org/context' + end + end end From 80ac2419f9f398938b7f7c0fa057d0dafd8ac7c5 Mon Sep 17 00:00:00 2001 From: Pascal Widdershoven Date: Sat, 16 Aug 2014 20:58:02 +0200 Subject: [PATCH 0051/1107] Refactor url normalization in StoryRepository --- app/repositories/story_repository.rb | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index ad6ee6a55..621a4ca8f 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -112,13 +112,10 @@ def self.expand_absolute_urls(content, base_url) end def self.normalize_url(url, base_url) - uri = URI.parse(url) - - unless uri.scheme - base_uri = URI.parse(base_url) - uri.scheme = base_uri.scheme || 'http' - end + uri = URI.parse(url) + base_uri = URI.parse(base_url) + uri.scheme ||= base_uri.scheme || 'http' uri.to_s end From 8da372d589505edaf84789c893514a4a897e6fdd Mon Sep 17 00:00:00 2001 From: Pascal Widdershoven Date: Sat, 16 Aug 2014 23:58:28 +0200 Subject: [PATCH 0052/1107] Handle cases where URI's cannot be parsed when expanding urls --- app/repositories/story_repository.rb | 7 ++++++- spec/repositories/story_repository_spec.rb | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index 04f0cbf8a..b28cdc66e 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -101,7 +101,12 @@ def self.expand_absolute_urls(content, base_url) doc.css("#{tag}[#{attr}]").each do |node| url = node.get_attribute(attr) unless url =~ abs_re - node.set_attribute(attr, URI.join(base_url, url).to_s) + begin + node.set_attribute(attr, URI.join(base_url, url).to_s) + rescue URI::InvalidURIError + # Just ignore. If we cannot parse the url, we dno't want the entire + # import to blow up. + end end end end diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 4c04ed06b..c15b975b2 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -52,6 +52,17 @@
    EOS end + + it "leaves the url as-is if it cannot be parsed" do + weird_url = "https://github.com/aphyr/jepsen/blob/1403f2d6e61c595bafede0d404fd4a893371c036/elasticsearch/src/jepsen/system/elasticsearch.clj#L161-L226.%20Then%20we'll%20write%20a%20%5Bregister%20test%5D(https://github.com/aphyr/jepsen/blob/1403f2d6e61c595bafede0d404fd4a893371c036/elasticsearch/test/jepsen/system/elasticsearch_test.clj#L18-L50)" + + content = <<-EOS + + EOS + + result = StoryRepository.expand_absolute_urls(content, "http://oodl.io/d/") + result.should include(weird_url) + end end describe ".extract_content" do From bf37db8bc3b9066abfda7484dbe8648c2dff6d9d Mon Sep 17 00:00:00 2001 From: Pascal Widdershoven Date: Mon, 18 Aug 2014 06:15:16 +0200 Subject: [PATCH 0053/1107] Fix comment typo --- app/repositories/story_repository.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index b28cdc66e..2ec644e83 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -104,7 +104,7 @@ def self.expand_absolute_urls(content, base_url) begin node.set_attribute(attr, URI.join(base_url, url).to_s) rescue URI::InvalidURIError - # Just ignore. If we cannot parse the url, we dno't want the entire + # Just ignore. If we cannot parse the url, we don't want the entire # import to blow up. end end From 19e5c6690fb32cc401c20b561287d45d81bb8313 Mon Sep 17 00:00:00 2001 From: Pascal Widdershoven Date: Mon, 18 Aug 2014 06:17:29 +0200 Subject: [PATCH 0054/1107] Refactor StoryRepository#normalize_url for improved readability --- app/repositories/story_repository.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index 621a4ca8f..f7a7e9561 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -115,7 +115,10 @@ def self.normalize_url(url, base_url) uri = URI.parse(url) base_uri = URI.parse(base_url) - uri.scheme ||= base_uri.scheme || 'http' + unless uri.scheme + uri.scheme = base_uri.scheme || 'http' + end + uri.to_s end From 3b965a4a672dc504bc4fa7089546c248d48018a6 Mon Sep 17 00:00:00 2001 From: Pascal Widdershoven Date: Mon, 18 Aug 2014 20:36:41 +0200 Subject: [PATCH 0055/1107] Ignore stories older than 3 days when importing feeds Timestamps in feeds can not be trusted but we not some form of preventing old articles be imported when a new feed is added or when this branch is merged and the way Stringer handles story updates changes. See discussion in https://github.com/swanson/stringer/issues/261 and https://github.com/swanson/stringer/pull/328. --- Gemfile | 3 ++- Gemfile.lock | 4 +++- app/commands/feeds/find_new_stories.rb | 13 ++++++++++++- spec/commands/find_new_stories_spec.rb | 25 +++++++++++++++++++++---- spec/integration/feed_importing_spec.rb | 11 +++++++++++ 5 files changed, 49 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index ecd70e909..72229f97d 100644 --- a/Gemfile +++ b/Gemfile @@ -19,7 +19,8 @@ group :development, :test do gem "rspec", "~> 2.14", ">= 2.14.1" gem "rspec-html-matchers", "~> 0.4.3" gem "shotgun", "~> 0.9.0" - gem "capybara" + gem "capybara", "~> 2.4.1" + gem "timecop", "~> 0.7.1" end gem "activerecord", "~> 4.0" diff --git a/Gemfile.lock b/Gemfile.lock index b80b46e1d..ce5079c57 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -149,6 +149,7 @@ GEM thread_safe (0.1.3) atomic tilt (1.4.1) + timecop (0.7.1) tins (0.13.1) tzinfo (0.3.38) unicorn (4.7.0) @@ -166,7 +167,7 @@ DEPENDENCIES activerecord (~> 4.0) arel! bcrypt-ruby (~> 3.1.2) - capybara + capybara (~> 2.4.1) coveralls (~> 0.7) delayed_job (~> 4.0) delayed_job_active_record (~> 4.0) @@ -193,5 +194,6 @@ DEPENDENCIES sinatra-flash (~> 0.3.0) sqlite3 (~> 1.3, >= 1.3.8) thread (~> 0.1.3) + timecop (~> 0.7.1) unicorn (~> 4.7) will_paginate (~> 3.0, >= 3.0.5) diff --git a/app/commands/feeds/find_new_stories.rb b/app/commands/feeds/find_new_stories.rb index b217102e0..823e48fa5 100644 --- a/app/commands/feeds/find_new_stories.rb +++ b/app/commands/feeds/find_new_stories.rb @@ -1,6 +1,8 @@ require_relative "../../repositories/story_repository" class FindNewStories + STORY_AGE_THRESHOLD_DAYS = 3 + def initialize(raw_feed, feed_id, last_fetched, latest_entry_id = nil) @raw_feed = raw_feed @feed_id = feed_id @@ -14,9 +16,18 @@ def new_stories @raw_feed.entries.each do |story| break if @latest_entry_id && story.id == @latest_entry_id - stories << story unless StoryRepository.exists?(story.id, @feed_id) + unless story_age_exceeds_threshold?(story) || StoryRepository.exists?(story.id, @feed_id) + stories << story + end end stories end + + private + + def story_age_exceeds_threshold?(story) + max_age = Time.now - STORY_AGE_THRESHOLD_DAYS.days + story.published && story.published < max_age + end end diff --git a/spec/commands/find_new_stories_spec.rb b/spec/commands/find_new_stories_spec.rb index cdf7e3478..8b7f45bfd 100644 --- a/spec/commands/find_new_stories_spec.rb +++ b/spec/commands/find_new_stories_spec.rb @@ -11,8 +11,8 @@ end it "should find zero new stories" do - story1 = double(id: "story1") - story2 = double(id: "story2") + story1 = double(published: nil, id: "story1") + story2 = double(published: nil, id: "story2") feed = double(entries: [story1, story2]) result = FindNewStories.new(feed, 1, Time.new(2013, 1, 2)).new_stories @@ -22,8 +22,8 @@ context "the feed contains new stories" do it "should return stories that are not found in the database" do - story1 = double(id: "story1") - story2 = double(id: "story2") + story1 = double(published: nil, id: "story1") + story2 = double(published: nil, id: "story2") feed = double(entries: [story1, story2]) StoryRepository.stub(:exists?).with("story1", 1).and_return(true) @@ -42,5 +42,22 @@ result = FindNewStories.new(feed, 1, Time.new(2013, 1, 3), "old-story").new_stories result.should eq [new_story] end + + it "should ignore stories older than 3 days" do + new_stories = [ + double(published: 1.hour.ago, id: "new-story"), + double(published: 2.days.ago, id: "new-story") + ] + + stories_older_than_3_days = [ + double(published: 3.days.ago, id: "new-story"), + double(published: 4.days.ago, id: "new-story") + ] + + feed = double(last_modified: nil, entries: new_stories + stories_older_than_3_days) + + result = FindNewStories.new(feed, 1, nil, nil).new_stories + result.should_not include(stories_older_than_3_days) + end end end diff --git a/spec/integration/feed_importing_spec.rb b/spec/integration/feed_importing_spec.rb index 42639bb59..08491e77f 100644 --- a/spec/integration/feed_importing_spec.rb +++ b/spec/integration/feed_importing_spec.rb @@ -4,6 +4,7 @@ require "support/feed_server" require "capybara" require "capybara/server" +require "timecop" app_require "tasks/fetch_feed" @@ -21,6 +22,12 @@ end describe "Valid feed" do + before(:all) do + # articles older than 3 days are ignored, so freeze time within + # applicable range of the stories in the sample feed + Timecop.freeze Time.parse("2014-08-15T17:30:00Z") + end + describe "Importing for the first time" do it "imports all entries" do @server.response = sample_data("feeds/feed01_valid_feed/feed.xml") @@ -51,6 +58,10 @@ end describe "Feed with incorrect pubdates" do + before(:all) do + Timecop.freeze Time.parse("2014-08-12T17:30:00Z") + end + context "has been fetched before" do it "imports all new stories" do # This spec describes a scenario where the feed is reporting incorrect From a6fc8e1d60dd494958f443b9f0d7d98d6102eb2e Mon Sep 17 00:00:00 2001 From: Pascal Widdershoven Date: Wed, 20 Aug 2014 07:52:24 +0200 Subject: [PATCH 0056/1107] Allow to enforce SSL Enabled when ENFORCE_SSL=true is set. --- Gemfile | 1 + Gemfile.lock | 3 +++ app.rb | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/Gemfile b/Gemfile index 6d535f8fd..ff93451fb 100644 --- a/Gemfile +++ b/Gemfile @@ -42,3 +42,4 @@ gem "sinatra-contrib", ">= 1.4.2" gem "sinatra-flash", "~> 0.3.0" gem "thread", "~> 0.1.3" gem "will_paginate", "~> 3.0", ">= 3.0.5" +gem "rack-ssl" diff --git a/Gemfile.lock b/Gemfile.lock index 87ad7486f..50a6d8d5d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -84,6 +84,8 @@ GEM rack (1.5.2) rack-protection (1.5.1) rack + rack-ssl (1.4.1) + rack rack-test (0.6.2) rack (>= 1.0) racksh (1.0.0) @@ -171,6 +173,7 @@ DEPENDENCIES nokogiri (~> 1.6) pg (~> 0.17.1) pry-byebug (~> 1.2) + rack-ssl rack-test (~> 0.6.2) racksh (~> 1.0) rake (~> 10.1, >= 10.1.1) diff --git a/app.rb b/app.rb index f36ee73b5..4eacb05a9 100644 --- a/app.rb +++ b/app.rb @@ -3,6 +3,7 @@ require "sinatra/flash" require "sinatra/contrib/all" require "sinatra/assetpack" +require "rack/ssl" require "json" require "i18n" require "will_paginate" @@ -15,6 +16,9 @@ I18n.config.enforce_available_locales=false class Stringer < Sinatra::Base + # need to exclude assets for sinatra assetpack, see https://github.com/swanson/stringer/issues/112 + use Rack::SSL, exclude: ->(env) { env['PATH_INFO'] =~ /^\/(js|css|img)/ } if ENV["ENFORCE_SSL"] == 'true' + register Sinatra::ActiveRecordExtension register Sinatra::Flash register Sinatra::Contrib From 51fa99cd755e92829ffd8fabc7aa983f11b39b4f Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Sun, 31 Aug 2014 10:43:18 +0200 Subject: [PATCH 0057/1107] Remove foreman from Gemfile > You should not put `foreman` into your `Gemfile` as the dependencies of a > developer utility should not be able to interfere with the dependencies of > your app. > Ruby users should take care *not* to install foreman in their project's > `Gemfile`. --- Gemfile | 1 - Gemfile.lock | 5 ----- README.md | 6 +++++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 23d90bce9..0abacd31e 100644 --- a/Gemfile +++ b/Gemfile @@ -13,7 +13,6 @@ end group :development, :test do gem "coveralls", "~> 0.7", require: false gem "faker", "~> 1.2" - gem "foreman", "~> 0.63.0" gem "pry-byebug", "~> 1.2" gem "rack-test", "~> 0.6.2" gem "rspec", "~> 2.14", ">= 2.14.1" diff --git a/Gemfile.lock b/Gemfile.lock index 9f69b811f..533e2dce6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -53,7 +53,6 @@ GEM delayed_job (>= 3.0, < 4.1) diff-lcs (1.2.5) docile (1.1.1) - dotenv (0.9.0) faker (1.2.0) i18n (~> 0.5) feedbag (0.9.2) @@ -62,9 +61,6 @@ GEM curb (~> 0.8.1) loofah (~> 2.0.0) sax-machine (~> 0.2.1) - foreman (0.63.0) - dotenv (>= 0.7) - thor (>= 0.13.6) highline (1.6.20) hpricot (0.8.6) i18n (0.6.9) @@ -176,7 +172,6 @@ DEPENDENCIES faker (~> 1.2) feedbag (~> 0.9.2) feedjira (~> 1.3.0) - foreman (~> 0.63.0) highline (~> 1.6, >= 1.6.20) i18n (~> 0.6.9) loofah (~> 2.0.0) diff --git a/README.md b/README.md index d28167ddf..cc733442d 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,11 @@ Run the Javascript tests with `rake test_js` and then open a browser to `http:// ### Getting Started -To get started using Stringer for development simply run the following: +To get started using Stringer for development you first need to install `foreman`. + + gem install foreman + +Then run the following commands. ```sh bundle install From 3430fb95feb363e65ad10bf954ddd1bbd834cba5 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Mon, 1 Sep 2014 22:22:06 +0200 Subject: [PATCH 0058/1107] Clean up Gemfile * Sort gem declarations lexicographically. * Use `arel` from RubyGems.org. * Add pessimistic version lock on `rack-ssl`. Fixes #334. --- Gemfile | 9 ++++----- Gemfile.lock | 12 +++--------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index 0abacd31e..61fdcb916 100644 --- a/Gemfile +++ b/Gemfile @@ -11,6 +11,7 @@ group :development do end group :development, :test do + gem "capybara", "~> 2.4.1" gem "coveralls", "~> 0.7", require: false gem "faker", "~> 1.2" gem "pry-byebug", "~> 1.2" @@ -18,13 +19,11 @@ group :development, :test do gem "rspec", "~> 2.14", ">= 2.14.1" gem "rspec-html-matchers", "~> 0.4.3" gem "shotgun", "~> 0.9.0" - gem "capybara", "~> 2.4.1" gem "timecop", "~> 0.7.1" end gem "activerecord", "~> 4.0" -# need to work around bug in 4.0.1 https://github.com/rails/arel/pull/216 -gem 'arel', git: 'git://github.com/rails/arel.git', branch: '4-0-stable' +gem "arel", "~> 4.0.2" gem "bcrypt-ruby", "~> 3.1.2" gem "delayed_job", "~> 4.0" gem "delayed_job_active_record", "~> 4.0" @@ -34,13 +33,13 @@ gem "highline", "~> 1.6", ">= 1.6.20", require: false gem "i18n", "~> 0.6.9" gem "loofah", "~> 2.0.0" gem "nokogiri", "~> 1.6" +gem "rack-ssl", "~> 1.4.1" gem "racksh", "~> 1.0" gem "rake", "~> 10.1", ">= 10.1.1" gem "sinatra", "~> 1.4", ">= 1.4.4" -gem "sinatra-assetpack", "~> 0.3.1", require: "sinatra/assetpack" gem "sinatra-activerecord", "~> 1.2", ">= 1.2.3" +gem "sinatra-assetpack", "~> 0.3.1", require: "sinatra/assetpack" gem "sinatra-contrib", ">= 1.4.2" gem "sinatra-flash", "~> 0.3.0" gem "thread", "~> 0.1.3" gem "will_paginate", "~> 3.0", ">= 3.0.5" -gem "rack-ssl" diff --git a/Gemfile.lock b/Gemfile.lock index 533e2dce6..a59e66162 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,10 +1,3 @@ -GIT - remote: git://github.com/rails/arel.git - revision: 454a25f18c95cdfba5520a6fc5bdb6d476e20a85 - branch: 4-0-stable - specs: - arel (4.0.1.20131022201058) - GEM remote: https://rubygems.org/ specs: @@ -23,6 +16,7 @@ GEM multi_json (~> 1.3) thread_safe (~> 0.1) tzinfo (~> 0.3.37) + arel (4.0.2) atomic (1.1.14) backports (3.3.5) bcrypt-ruby (3.1.2) @@ -163,7 +157,7 @@ PLATFORMS DEPENDENCIES activerecord (~> 4.0) - arel! + arel (~> 4.0.2) bcrypt-ruby (~> 3.1.2) capybara (~> 2.4.1) coveralls (~> 0.7) @@ -178,7 +172,7 @@ DEPENDENCIES nokogiri (~> 1.6) pg (~> 0.17.1) pry-byebug (~> 1.2) - rack-ssl + rack-ssl (~> 1.4.1) rack-test (~> 0.6.2) racksh (~> 1.0) rake (~> 10.1, >= 10.1.1) From 176c084390193d4816a008313821fac49892d98b Mon Sep 17 00:00:00 2001 From: Pascal Widdershoven Date: Thu, 4 Sep 2014 15:14:54 +0200 Subject: [PATCH 0059/1107] Upgrade rack-protection to workaround issues with recent Chrome updates Fixes https://github.com/swanson/stringer/issues/335 --- Gemfile | 1 + Gemfile.lock | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 61fdcb916..78bca7a4c 100644 --- a/Gemfile +++ b/Gemfile @@ -43,3 +43,4 @@ gem "sinatra-contrib", ">= 1.4.2" gem "sinatra-flash", "~> 0.3.0" gem "thread", "~> 0.1.3" gem "will_paginate", "~> 3.0", ">= 3.0.5" +gem "rack-protection", "~> 1.5.3" diff --git a/Gemfile.lock b/Gemfile.lock index a59e66162..3171b3f19 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,7 +78,7 @@ GEM byebug (~> 2.2) pry (~> 0.9.12) rack (1.5.2) - rack-protection (1.5.1) + rack-protection (1.5.3) rack rack-ssl (1.4.1) rack @@ -172,6 +172,7 @@ DEPENDENCIES nokogiri (~> 1.6) pg (~> 0.17.1) pry-byebug (~> 1.2) + rack-protection (~> 1.5.3) rack-ssl (~> 1.4.1) rack-test (~> 0.6.2) racksh (~> 1.0) From c88180492e9ddc45fdb8c72cf7d3c9df1deade4b Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Mon, 1 Sep 2014 22:49:26 +0200 Subject: [PATCH 0060/1107] Replace highline dependency with standard Ruby There are two minor differences from the `highline` version. * Character echoing on password input has been disabled altogether. This is due to limitations of the `console/io` library. * The password length validation has been removed. There's no feature like this in the `console/io` library and there's no validation like this on the model. Instead of reimplementing it, I think we might as well remove it. The test for `ChangePassword` could use some improvement, especially to avoid stubbing a method on the object under test. The change to `ChangeUserPassword` is tangential, but fixes a bug where the password wouldn't be updated due to a failing validation (`password` and `password_confirmation` don't match). --- Gemfile | 1 - Gemfile.lock | 2 -- app/commands/users/change_user_password.rb | 2 +- app/tasks/change_password.rb | 22 ++++++++++------------ spec/tasks/change_password_spec.rb | 8 +++----- 5 files changed, 14 insertions(+), 21 deletions(-) diff --git a/Gemfile b/Gemfile index 0abacd31e..94e2914e9 100644 --- a/Gemfile +++ b/Gemfile @@ -30,7 +30,6 @@ gem "delayed_job", "~> 4.0" gem "delayed_job_active_record", "~> 4.0" gem "feedbag", "~> 0.9.2" gem "feedjira", "~> 1.3.0" -gem "highline", "~> 1.6", ">= 1.6.20", require: false gem "i18n", "~> 0.6.9" gem "loofah", "~> 2.0.0" gem "nokogiri", "~> 1.6" diff --git a/Gemfile.lock b/Gemfile.lock index 533e2dce6..6f910f248 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,7 +61,6 @@ GEM curb (~> 0.8.1) loofah (~> 2.0.0) sax-machine (~> 0.2.1) - highline (1.6.20) hpricot (0.8.6) i18n (0.6.9) jsmin (1.0.1) @@ -172,7 +171,6 @@ DEPENDENCIES faker (~> 1.2) feedbag (~> 0.9.2) feedjira (~> 1.3.0) - highline (~> 1.6, >= 1.6.20) i18n (~> 0.6.9) loofah (~> 2.0.0) nokogiri (~> 1.6) diff --git a/app/commands/users/change_user_password.rb b/app/commands/users/change_user_password.rb index 72ce8cce2..a818aea13 100644 --- a/app/commands/users/change_user_password.rb +++ b/app/commands/users/change_user_password.rb @@ -8,7 +8,7 @@ def initialize(repository = UserRepository) def change_user_password(new_password) user = @repo.first - user.password = new_password + user.password = user.password_confirmation = new_password user.api_key = ApiKey.compute(new_password) @repo.save(user) diff --git a/app/tasks/change_password.rb b/app/tasks/change_password.rb index 517552670..f67b09805 100644 --- a/app/tasks/change_password.rb +++ b/app/tasks/change_password.rb @@ -1,25 +1,23 @@ -require "highline" +require "io/console" + require_relative "../commands/users/change_user_password" class ChangePassword - def initialize(ui = HighLine.new, command = ChangeUserPassword.new) - @ui = ui + def initialize(command = ChangeUserPassword.new) @command = command end def change_password while (password = ask_password) != (confirmation = ask_confirmation) - @ui.say "The confirmation doesn't match the password. Please try again." + puts "The confirmation doesn't match the password. Please try again." end @command.change_user_password(password) end private + def ask_password - ask_hidden("New password: ") do |q| - q.validate = /\A.+\Z/ - q.responses[:not_valid] = "The password can't be blank." - end + ask_hidden("New password: ") end def ask_confirmation @@ -27,9 +25,9 @@ def ask_confirmation end def ask_hidden(question) - @ui.ask(question) do |q| - q.echo = "*" - yield(q) if block_given? - end + print(question) + input = STDIN.noecho(&:gets).chomp + puts + input end end diff --git a/spec/tasks/change_password_spec.rb b/spec/tasks/change_password_spec.rb index ddff1273d..b6b8bed10 100644 --- a/spec/tasks/change_password_spec.rb +++ b/spec/tasks/change_password_spec.rb @@ -3,15 +3,14 @@ app_require "tasks/change_password" describe ChangePassword do - let(:ui) { double("ui") } let(:command) { double("command") } let(:new_password) { "new-pw" } - let(:task) { ChangePassword.new(ui, command) } + let(:task) { ChangePassword.new(command) } describe "#change_password" do it "invokes command with confirmed password" do - ui.should_receive(:ask).twice + task.should_receive(:ask_hidden).twice .and_return(new_password, new_password) command @@ -22,10 +21,9 @@ end it "repeats until a matching confirmation" do - ui.should_receive(:ask).exactly(2).times + task.should_receive(:ask_hidden).exactly(2).times .and_return(new_password, "", new_password, new_password) - ui.should_receive(:say).with(/match/) command .should_receive(:change_user_password) .with(new_password) From dec9db511eea25a57282f7873b610c16a8102a1f Mon Sep 17 00:00:00 2001 From: Jason Ng PT Date: Wed, 10 Sep 2014 15:01:05 +0800 Subject: [PATCH 0061/1107] Update OpenShift Docs for Ruby 2.0 Cartridge Update the instructions for OpenShift for the newly added Ruby 2.0 Cartridge. Due to an older version of Bundler, some workarounds have to be included. --- docs/OpenShift.md | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/docs/OpenShift.md b/docs/OpenShift.md index ab1bb9b7d..ff002edcb 100644 --- a/docs/OpenShift.md +++ b/docs/OpenShift.md @@ -4,10 +4,10 @@ Stringer on OpenShift Deploying into OpenShift ------------------------ -1. Creating new OpenShift Ruby 1.9 application with the Postgresql cartridge (command-line). +1. Creating new OpenShift Ruby 2.0 application with the Postgresql cartridge (command-line). ```sh - rhc app create feeds ruby-1.9 postgresql-9.2 + rhc app create feeds ruby-2.0 postgresql-9.2 ``` 2. Pull the code from the Stringer Github repository. @@ -32,7 +32,7 @@ Deploying into OpenShift openssl rand -hex 20 ``` -5. Add the generated secret into a new file, .openshift/action_hooks/pre_start_ruby-1.9, in the format below. +5. Add the generated secret into a new file, .openshift/action_hooks/pre_start_ruby-2.0, in the format below. ``` export SECRET_TOKEN="generated_secret" @@ -41,7 +41,7 @@ Deploying into OpenShift 6. Make sure that the 2 files created above are executable on Unix-like systems. ```sh - chmod +x .openshift/action_hooks/deploy .openshift/action_hooks/pre_start_ruby-1.9 + chmod +x .openshift/action_hooks/deploy .openshift/action_hooks/pre_start_ruby-2.0 ``` 7. Configuration of the database server is next. Open the file config/database.yml and add in the configuration for Production as shown below. OpenShift is able to use environment variables to push the information into the application. @@ -56,25 +56,27 @@ Deploying into OpenShift password: <%= ENV["OPENSHIFT_POSTGRESQL_DB_PASSWORD"] %> ``` -8. Due to an older version of bundler being used in OpenShift (1.1.4), it does not support indicating the ruby version in the Gemfile. Remove the line from the Gemfile below. (Referencing issue [#266](https://github.com/swanson/stringer/issues/266)) +8. Due to an older version of bundler being used in OpenShift (1.3.5), some changes need to be made in the Gemfile. + + Remove the Ruby version specification from the Gemfile below (error reporting wrong Ruby version when deploying to OpenShift). ``` - ruby '2.0.0' + ruby '2.0.0' ``` -9. As OpenShift is still using Ruby 1.9.3 and the [gem 'pry-byebug'](https://github.com/deivid-rodriguez/pry-byebug) needs Ruby 2.0, we can try to just install the production environment from the Gemfile but there seems to be a [bug in OpenShift](https://bugzilla.redhat.com/show_bug.cgi?id=1049411). A temporary work-around is to remove the 'pry-byebug' gem in the Gemfile. Note that this is only for deploying into OpenShift production. (Referencing issue [#294](https://github.com/swanson/stringer/pull/294) ) + Then change the two gem dependencies below to use the hash rocket syntax for the "require" option. ``` - gem "pry-byebug", "~> 1.2" + gem "coveralls", "~> 0.7", require: false + gem "sinatra-assetpack", "~> 0.3.1", require: "sinatra/assetpack" ``` - - After removing the `pry-byebug` gem from `Gemfile`, the bundle has to be updated. - - ```sh - bundle install + to + ``` + gem "coveralls", "~> 0.7", :require => false + gem "sinatra-assetpack", "~> 0.3.1", :require => "sinatra/assetpack" ``` -10. Finally, once completed, all changes should be committed and pushed to OpenShift. Note that it might take a while when pushing to OpenShift. +9. Finally, once completed, all changes should be committed and pushed to OpenShift. Note that it might take a while when pushing to OpenShift. ```sh git add . @@ -82,7 +84,7 @@ Deploying into OpenShift git push origin ``` -11. Check that you are able to access the website at the URL given, i.e. feeds-username.rhcloud.com. Then set your password, import your feeds and all good to go! +10. Check that you are able to access the website at the URL given, i.e. feeds-username.rhcloud.com. Then set your password, import your feeds and all good to go! Adding Cronjob to Fetch Feeds From cbeb491a06c07812892290f36ba242c04a5acdf0 Mon Sep 17 00:00:00 2001 From: Jason Ng PT Date: Wed, 10 Sep 2014 18:20:45 +0800 Subject: [PATCH 0062/1107] Set SECRET_TOKEN as OpenShift Environment Variable This sets the SECRET_TOKEN variable as OpenShift Environment Variable, rather than committing the SECRET_TOKEN to the repo. [skip ci] --- docs/OpenShift.md | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/docs/OpenShift.md b/docs/OpenShift.md index ff002edcb..fd07d673a 100644 --- a/docs/OpenShift.md +++ b/docs/OpenShift.md @@ -26,25 +26,19 @@ Deploying into OpenShift popd > /dev/null ``` -4. Next, a secret is needed for the application. Generate the secret by running: +4. Make sure that the file created above is executable on Unix-like systems. ```sh - openssl rand -hex 20 + chmod +x .openshift/action_hooks/deploy ``` -5. Add the generated secret into a new file, .openshift/action_hooks/pre_start_ruby-2.0, in the format below. - - ``` - export SECRET_TOKEN="generated_secret" - ``` - -6. Make sure that the 2 files created above are executable on Unix-like systems. +5. Set the SECRET_TOKEN as a rhc environment variable by generating it with the command below. ```sh - chmod +x .openshift/action_hooks/deploy .openshift/action_hooks/pre_start_ruby-2.0 + rhc env set SECRET_TOKEN="`openssl rand -hex 20`" ``` -7. Configuration of the database server is next. Open the file config/database.yml and add in the configuration for Production as shown below. OpenShift is able to use environment variables to push the information into the application. +6. Configuration of the database server is next. Open the file config/database.yml and add in the configuration for Production as shown below. OpenShift is able to use environment variables to push the information into the application. ``` production: @@ -56,7 +50,7 @@ Deploying into OpenShift password: <%= ENV["OPENSHIFT_POSTGRESQL_DB_PASSWORD"] %> ``` -8. Due to an older version of bundler being used in OpenShift (1.3.5), some changes need to be made in the Gemfile. +7. Due to an older version of bundler being used in OpenShift (1.3.5), some changes need to be made in the Gemfile. Remove the Ruby version specification from the Gemfile below (error reporting wrong Ruby version when deploying to OpenShift). @@ -76,7 +70,7 @@ Deploying into OpenShift gem "sinatra-assetpack", "~> 0.3.1", :require => "sinatra/assetpack" ``` -9. Finally, once completed, all changes should be committed and pushed to OpenShift. Note that it might take a while when pushing to OpenShift. +8. Finally, once completed, all changes should be committed and pushed to OpenShift. Note that it might take a while when pushing to OpenShift. ```sh git add . @@ -84,7 +78,7 @@ Deploying into OpenShift git push origin ``` -10. Check that you are able to access the website at the URL given, i.e. feeds-username.rhcloud.com. Then set your password, import your feeds and all good to go! +9. Check that you are able to access the website at the URL given, i.e. feeds-username.rhcloud.com. Then set your password, import your feeds and all good to go! Adding Cronjob to Fetch Feeds From e91597f10c5a42d23268cb734062324d4d46fabc Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Sat, 6 Sep 2014 18:49:58 +0200 Subject: [PATCH 0063/1107] Fix FeverAPI test flakiness Attempts to address #331, mainly by stubbing `Time.now`. * Adds whitespace, splitting tests into setup, execution and verification steps. * Fixes a typo (`standart` -> `standard`). * Introduces `last_response_as_object` helper. This allows splitting the assertions into two parts, one asserting inclusion of standard keys and one asserting inclusion of keys specific to each action. * Stubs `Time.now`, splits assertions and makes use of the includes matcher. --- spec/fever_api_spec.rb | 127 ++++++++++++++++++++++++++++++----------- 1 file changed, 95 insertions(+), 32 deletions(-) diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index 0fc48fb2f..fc4dc38af 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -14,12 +14,20 @@ def app let(:group) { GroupFactory.build } let(:feed) { FeedFactory.build(group_id: group.id) } let(:stories) { [story_one, story_two] } - let(:answer) { { api_version: 3, auth: 1, last_refreshed_on_time: Time.now.to_i } } + let(:standard_answer) do + { api_version: 3, auth: 1, last_refreshed_on_time: 123456789 } + end let(:headers) { { api_key: api_key } } before do user = double(api_key: api_key) - User.stub(:first).and_return(user) + User.stub(:first) { user } + + Time.stub(:now) { Time.at(123456789) } + end + + def last_response_as_object + JSON.parse(last_response.body, symbolize_names: true) end describe "authentication" do @@ -44,83 +52,126 @@ def make_request(extra_headers = {}) get "/", headers.merge(extra_headers) end - it "returns standart answer" do + it "returns standard answer" do make_request + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) end it "returns groups and feeds by groups when 'groups' header is provided" do GroupRepository.stub(:list).and_return([group]) FeedRepository.stub_chain(:in_group, :order).and_return([feed]) + make_request(groups: nil) - answer.merge!(groups: [group.as_fever_json], feeds_groups: [{ group_id: group.id, feed_ids: feed.id.to_s }]) + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) + last_response_as_object.should include( + groups: [group.as_fever_json], + feeds_groups: [{ group_id: group.id, feed_ids: feed.id.to_s }] + ) end it "returns feeds and feeds by groups when 'feeds' header is provided" do FeedRepository.stub(:list).and_return([feed]) FeedRepository.stub_chain(:in_group, :order).and_return([feed]) + make_request(feeds: nil) - answer.merge!(feeds: [feed.as_fever_json], feeds_groups: [{ group_id: group.id, feed_ids: feed.id.to_s }]) + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) + last_response_as_object.should include( + feeds: [feed.as_fever_json], + feeds_groups: [{ group_id: group.id, feed_ids: feed.id.to_s }] + ) end it "returns favicons hash when 'favicons' header provided" do make_request(favicons: nil) - answer.merge!(favicons: [{ id: 0, data: "image/gif;base64,R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" }]) + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) + last_response_as_object.should include( + favicons: [ + { + id: 0, + data: "image/gif;base64,R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" + } + ] + ) end it "returns stories when 'items' header is provided along with 'since_id'" do StoryRepository.should_receive(:unread_since_id).with('5').and_return([story_one]) StoryRepository.should_receive(:unread).and_return([story_one, story_two]) + make_request(items: nil, since_id: 5) - answer.merge!(items: [story_one.as_fever_json], total_items: 2) + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) + last_response_as_object.should include( + items: [story_one.as_fever_json], + total_items: 2 + ) end it "returns stories when 'items' header is provided without 'since_id'" do StoryRepository.should_receive(:unread).twice.and_return([story_one, story_two]) + make_request(items: nil) - answer.merge!(items: [story_one.as_fever_json, story_two.as_fever_json], total_items: 2) + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) + last_response_as_object.should include( + items: [story_one.as_fever_json, story_two.as_fever_json], + total_items: 2 + ) end it "returns stories ids when 'items' header is provided along with 'with_ids'" do StoryRepository.should_receive(:fetch_by_ids).twice.with(['5']).and_return([story_one]) + make_request(items: nil, with_ids: 5) - answer.merge!(items: [story_one.as_fever_json], total_items: 1) + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) + last_response_as_object.should include( + items: [story_one.as_fever_json], + total_items: 1 + ) end it "returns links as empty array when 'links' header is provided" do make_request(links: nil) - answer.merge!(links: []) + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) + last_response_as_object.should include(links: []) end it "returns unread items ids when 'unread_item_ids' header is provided" do StoryRepository.should_receive(:unread).and_return([story_one, story_two]) + make_request(unread_item_ids: nil) - answer.merge!(unread_item_ids: [story_one.id,story_two.id].join(',')) + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) + last_response_as_object.should include( + unread_item_ids: [story_one.id,story_two.id].join(',') + ) end it "returns starred items when 'saved_item_ids' header is provided" do Story.should_receive(:where).with({ is_starred: true }).and_return([story_one, story_two]) + make_request(saved_item_ids: nil) - answer.merge!(saved_item_ids: [story_one.id,story_two.id].join(',')) + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) + last_response_as_object.should include( + saved_item_ids: [story_one.id,story_two.id].join(',') + ) end end @@ -131,44 +182,56 @@ def make_request(extra_headers = {}) it "commands to mark story as read" do MarkAsRead.should_receive(:new).with('10').and_return(double(mark_as_read: true)) - make_request({ mark: 'item', as: 'read', id: 10 }) + + make_request(mark: 'item', as: 'read', id: 10) + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) end it "commands to mark story as unread" do MarkAsUnread.should_receive(:new).with('10').and_return(double(mark_as_unread: true)) - make_request({ mark: 'item', as: 'unread', id: 10 }) + + make_request(mark: 'item', as: 'unread', id: 10) + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) end it "commands to save story" do MarkAsStarred.should_receive(:new).with('10').and_return(double(mark_as_starred: true)) - make_request({ mark: 'item', as: 'saved', id: 10 }) + + make_request(mark: 'item', as: 'saved', id: 10) + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) end it "commands to unsave story" do MarkAsUnstarred.should_receive(:new).with('10').and_return(double(mark_as_unstarred: true)) - make_request({ mark: 'item', as: 'unsaved', id: 10 }) + + make_request(mark: 'item', as: 'unsaved', id: 10) + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) end it "commands to mark group as read" do MarkGroupAsRead.should_receive(:new).with('10', '1375080946').and_return(double(mark_group_as_read: true)) + make_request(mark: 'group', as: 'read', id: 10, before: 1375080946) + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) end it "commands to mark entire feed as read" do MarkFeedAsRead.should_receive(:new).with('20', '1375080945').and_return(double(mark_feed_as_read: true)) + make_request(mark: 'feed', as: 'read', id: 20, before: 1375080945) + last_response.should be_ok - last_response.body.should == answer.to_json + last_response_as_object.should include(standard_answer) end end end From 3746379d79d34a09d2a3421af788409b6774c405 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Mon, 22 Sep 2014 11:44:03 +0200 Subject: [PATCH 0064/1107] Bump `backports`, `multi_json` and `sinatra` This is the result of running `bundle update sinatra-contrib` and fixes the following error when deploying to Heroku. > Could not detect rake tasks > ensure you can run `$ bundle exec rake -P` against your app with no environment variables present > and using the production group of your Gemfile. > This may be intentional, if you expected rake tasks to be run > cancel the build (CTRL+C) and fix the error then commit the fix: > rake aborted! > Bad file descriptor - bundle/ruby/2.0.0/gems/backports-3.3.5/lib/backports/1.9.1/io/open.rb > bundle/ruby/2.0.0/gems/backports-3.3.5/lib/backports/1.9.1/io/open.rb:2:in `close' > bundle/ruby/2.0.0/gems/backports-3.3.5/lib/backports/1.9.1/io/open.rb:2:in `open' > bundle/ruby/2.0.0/gems/backports-3.3.5/lib/backports/1.9.1/io/open.rb:2:in `' > bundle/ruby/2.0.0/gems/backports-3.3.5/lib/backports/tools.rb:328:in `require' > bundle/ruby/2.0.0/gems/backports-3.3.5/lib/backports/tools.rb:328:in `require_with_backports' > ... See for the full stack trace. Fixes #343. --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 55e27a020..cdd801146 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,7 +18,7 @@ GEM tzinfo (~> 0.3.37) arel (4.0.2) atomic (1.1.14) - backports (3.3.5) + backports (3.6.1) bcrypt-ruby (3.1.2) builder (3.1.4) byebug (2.5.0) @@ -65,7 +65,7 @@ GEM mime-types (2.0) mini_portile (0.5.2) minitest (4.7.5) - multi_json (1.8.2) + multi_json (1.10.1) nokogiri (1.6.1) mini_portile (~> 0.5.0) pg (0.17.1) @@ -110,7 +110,7 @@ GEM multi_json simplecov-html (~> 0.8.0) simplecov-html (0.8.0) - sinatra (1.4.4) + sinatra (1.4.5) rack (~> 1.4) rack-protection (~> 1.4) tilt (~> 1.3, >= 1.3.4) From 37b22a25cb1e4ef8cf7183efbf2fc819cba2eb93 Mon Sep 17 00:00:00 2001 From: davebradford Date: Fri, 17 Oct 2014 01:51:34 +0100 Subject: [PATCH 0065/1107] Add instructions to install foreman Since pull request #336 https://github.com/swanson/stringer/pull/336 foreman is no longer part of the Gemfile. This needs installing separately as it's not covered by the bundle install (which uses the Gemfile). Finally bundle exec will no longer work on foreman as it's no longer part of the Gemfile so removed the "bundle exec" prefix for starting the application. --- docs/VPS.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/VPS.md b/docs/VPS.md index b1a787dec..964cb6fb5 100644 --- a/docs/VPS.md +++ b/docs/VPS.md @@ -69,6 +69,10 @@ We also need to install bundler which will handle Stringer's dependencies gem install bundler rbenv rehash + +We will also need foreman to run our app + + gem install foreman Install Stringer and set it up ============================== @@ -99,7 +103,7 @@ Tell stringer to run the database in production mode, using the postgres databas Run the application: - bundle exec foreman start + foreman start Set up a cron job to parse the rss feeds. From 4549238deb76538493b44975ce16496ffcf668da Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Mon, 6 Oct 2014 20:37:11 +0200 Subject: [PATCH 0066/1107] Mention `b` in the list of shortcuts --- app/views/partials/_shortcuts.erb | 2 +- config/locales/de.yml | 2 +- config/locales/el-GR.yml | 2 +- config/locales/en.yml | 2 +- config/locales/es.yml | 2 +- config/locales/fr.yml | 2 +- config/locales/he.yml | 2 +- config/locales/it.yml | 2 +- config/locales/ja.yml | 2 +- config/locales/nl.yml | 2 +- config/locales/pt-BR.yml | 2 +- config/locales/pt.yml | 2 +- config/locales/ru.yml | 2 +- config/locales/sv.yml | 2 +- config/locales/tr.yml | 2 +- config/locales/zh-CN.yml | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/views/partials/_shortcuts.erb b/app/views/partials/_shortcuts.erb index e149a32b3..94e255753 100644 --- a/app/views/partials/_shortcuts.erb +++ b/app/views/partials/_shortcuts.erb @@ -10,7 +10,7 @@
  • o <%= t('partials.shortcuts.keys.or') %> enter: <%= t('partials.shortcuts.keys.oenter') %>
  • m: <%= t('partials.shortcuts.keys.m') %>
  • s: <%= t('partials.shortcuts.keys.s') %>
  • -
  • v: <%= t('partials.shortcuts.keys.v') %>
  • +
  • b <%= t('partials.shortcuts.keys.or') %> v: <%= t('partials.shortcuts.keys.bv') %>
  • a: <%= t('partials.shortcuts.keys.a') %>
  • shift+a: <%= t('partials.shortcuts.keys.shifta') %>
  • diff --git a/config/locales/de.yml b/config/locales/de.yml index 882a612b3..d33518a78 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -96,6 +96,7 @@ de: shortcuts: keys: a: Einen Feed hinzufügen + bv: Öffne die URL der Geschichte f: jk: Nächste/vorherige Geschichte left: Vorige Seite @@ -107,7 +108,6 @@ de: right: Nächste Seite s: Eintrag als Favorit markieren shifta: Alle als gelesen markieren - v: Öffne die URL der Geschichte title: Tastaturkürzel zen: archive: alle Einträge anzeigen diff --git a/config/locales/el-GR.yml b/config/locales/el-GR.yml index ba7cad7b5..79f70e364 100644 --- a/config/locales/el-GR.yml +++ b/config/locales/el-GR.yml @@ -96,6 +96,7 @@ el-GR: shortcuts: keys: a: + bv: Πήγαινε στην διεύθυνση f: jk: Επόμενη/Προηγούμενη είδηση left: @@ -107,7 +108,6 @@ el-GR: right: s: shifta: Επισήμανση όλων ως αναγνωσμένα - v: Πήγαινε στην διεύθυνση title: Συντομεύσεις στο πληκτρολόγιο zen: archive: εμφάνισης όλων των αντικειμένων diff --git a/config/locales/en.yml b/config/locales/en.yml index ec62a3f53..ee60d8bfe 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -96,6 +96,7 @@ en: shortcuts: keys: a: Add a feed + bv: Go to story URL f: Go to feeds page jk: Next/previous story left: Previous page @@ -107,7 +108,6 @@ en: right: Next page s: Mark item as starred/unstarred shifta: Mark all as read - v: Go to story URL title: Keyboard shortcuts zen: archive: view all items diff --git a/config/locales/es.yml b/config/locales/es.yml index 93854e54a..c82c74f91 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -96,6 +96,7 @@ es: shortcuts: keys: a: Añadir una feed + bv: Ir a URL de historia f: jk: Siguiente/previa historia left: Página anterior @@ -107,7 +108,6 @@ es: right: Siguiente página s: Marcar item como destacado/no destacado shifta: Marcar todo como leído - v: Ir a URL de historia title: Atajos de teclado zen: archive: ver todos los itemes diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 83c32960c..172e6328a 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -96,6 +96,7 @@ fr: shortcuts: keys: a: Ajouter un flux + bv: Aller à l'URL de l'article f: jk: Article suivant/précédent left: Page précédente @@ -107,7 +108,6 @@ fr: right: Page suivante s: Mettre/enlever cet article des favoris shifta: Tout marquer comme lu - v: Aller à l'URL de l'article title: Raccourcis clavier zen: archive: voir toutes les entrées diff --git a/config/locales/he.yml b/config/locales/he.yml index 7cc1cff2f..9e6d2c96d 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -96,6 +96,7 @@ he: shortcuts: keys: a: + bv: תעבור לקישור של הסיפור f: jk: סיפור הבא/קודם left: @@ -107,7 +108,6 @@ he: right: s: shifta: סמן הכל כנקרא - v: תעבור לקישור של הסיפור title: קיצורי מקשים zen: archive: ראה את כל הפריטים diff --git a/config/locales/it.yml b/config/locales/it.yml index db58281b7..d0b4cbaea 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -96,6 +96,7 @@ it: shortcuts: keys: a: Aggiungi un feed + bv: Vai all'URL della storia f: jk: Prossima/precedente storia left: Pagina precedente @@ -107,7 +108,6 @@ it: right: Pagina successiva s: Segna come preferita/non preferita shifta: Segna tutte le storie come lette - v: Vai all'URL della storia title: Shortcuts da tastiera zen: archive: vai all'archivio diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 3ec0beade..f09288354 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -96,6 +96,7 @@ ja: shortcuts: keys: a: フィードを追加 + bv: ストーリーURLを開く f: jk: 次/前のストーリー left: 前ページ @@ -107,7 +108,6 @@ ja: right: 次ページ s: スターを付ける/外す shifta: すべてのストーリーを既読にする - v: ストーリーURLを開く title: キーボードショートカット zen: archive: すべてのアイテムをみる diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 78de1693c..c94c42d57 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -96,6 +96,7 @@ nl: shortcuts: keys: a: Een feed toevoegen + bv: Open de URL van het artikel f: jk: Volgend/vorig artikel left: Vorige pagina @@ -107,7 +108,6 @@ nl: right: Volgende pagina s: Artikel markeren met ster/zonder ster shifta: Alles als gelezen markeren - v: Open de URL van het artikel title: Sneltoetsen zen: archive: alle artikelen bekijken diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index f0cadc159..0a18bfe00 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -96,6 +96,7 @@ pt-BR: shortcuts: keys: a: Adicione um feed + bv: Ir para a URL da história f: jk: História próxima/anterior left: Página anterior @@ -107,7 +108,6 @@ pt-BR: right: Próxima Página s: Marcar item com estrela/Remover estrela shifta: Marcar tudo como lido - v: Ir para a URL da história title: Atalhos de teclado zen: archive: visualizar todos os itens diff --git a/config/locales/pt.yml b/config/locales/pt.yml index a91f6bd69..054b19bb8 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -96,6 +96,7 @@ pt: shortcuts: keys: a: + bv: Ir para a URL da história f: jk: Próxima história/História anterior left: @@ -107,7 +108,6 @@ pt: right: s: shifta: Marcar tudo como lido - v: Ir para a URL da história title: Atalhos do teclado zen: archive: visualizar todos os itens diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 9ecd00fe8..30299b1ab 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -96,6 +96,7 @@ ru: shortcuts: keys: a: + bv: Перейти к ссылке f: jk: Перейти на следующую/предыдущую историю left: Предыдущая страница @@ -107,7 +108,6 @@ ru: right: Следующая страница s: Отметить как избранное/обычное shifta: Пометить всё как прочитанное - v: Перейти к ссылке title: Быстрые комбинации клавиш zen: archive: просмотреть все фиды diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 41c85af63..12487e118 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -96,6 +96,7 @@ sv: shortcuts: keys: a: Lägg till en feed + bv: Gå till berättelsens URL f: Gå till feed-sidan jk: Nästa/föregående berättelse left: Föregående sida @@ -107,7 +108,6 @@ sv: right: Nästa sida s: Markera som stjärnmärkt/ej stjärnmärkt shifta: Markera alla som lästa - v: Gå till berättelsens URL title: Kortkommandon zen: archive: visa alla objekt diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 742b77c43..26523682f 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -96,6 +96,7 @@ tr: shortcuts: keys: a: + bv: Hikaye URL'sine git f: jk: Sonraki/onceki hikaye left: @@ -107,7 +108,6 @@ tr: right: s: shifta: Hepsini okundu olarak isaretle - v: Hikaye URL'sine git title: Klavye kisayollari zen: archive: butun hikayeleri goster diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 3e02eeaff..275dee1ad 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -96,6 +96,7 @@ zh-CN: shortcuts: keys: a: 添加新订阅 + bv: 转到原网址 f: jk: 下一个/上一个故事 left: 上一页 @@ -107,7 +108,6 @@ zh-CN: right: 下一页 s: 将条目标为加注星标/取消星标 shifta: 全部标为已读 - v: 转到原网址 title: 快捷键 zen: archive: 查看所有故事 From 5f053d737834fdc4ecf7fbe0b4da986ab75f0cc1 Mon Sep 17 00:00:00 2001 From: Matt Swanson Date: Fri, 24 Oct 2014 13:12:53 -0400 Subject: [PATCH 0067/1107] Update translations --- config/locales/eo.yml | 156 ++++++++++++++++++++++++++++++++++++++++++ config/locales/es.yml | 16 ++--- config/locales/ja.yml | 20 +++--- config/locales/nl.yml | 26 +++---- 4 files changed, 187 insertions(+), 31 deletions(-) create mode 100644 config/locales/eo.yml diff --git a/config/locales/eo.yml b/config/locales/eo.yml new file mode 100644 index 000000000..36b234075 --- /dev/null +++ b/config/locales/eo.yml @@ -0,0 +1,156 @@ +eo: + archive: + next: Sekva + of: de + previous: Antaŭ + sorry: Bedaŭrinde, vi ankoraŭ ne legis ajnajn rakontojn. + date: + abbr_month_names: + - + - jan + - feb + - mar + - apr + - maj + - jun + - jul + - aŭg + - sep + - okt + - nov + - dec + feeds: + add: + description: Algluu la URL de la blogo kiu vi volas legi. + fields: + feed_url: URL de fluo + submit: Aldoni + flash: + added_successfully: Ni aldonis vian nova fluon. Revenu pli poste. + already_subscribed_error: Vi abonis tiun fluon jam... + feed_not_found_error: Ni ne eblas trovi tion fluon. Reprovu. + title: Ĉu vi bezonas novajn rakontojn? + edit: + fields: + feed_name: Nomo de fluo + feed_url: URL de fluo + submit: Konservi + flash: + updated_successfully: La fluo ĝisdatiĝis por vi. + index: + add: aldoni + add_some_feeds: He, vi devas %{add} kelkajn fluojn. + first_run: + password: + anti_social: kontraŭsocia + description: Ek elektas pasvorton por ke sole vi eblas legi viajn rakontojn. + fields: + next: Sekva + password: Pasvorto + password_confirmation: Konfirmi + flash: + passwords_dont_match: He, vian pasvorta konfirmacio ne kongruis. Reprovu. + subtitle: 'Estas unu uzanto: vi.' + title: Stringer estas + flash: + cookies_required: Nu, mallerteco. Kuketoj estas postulita por funkcii dece. + js_required: Nu, mallerteco. JavaScript estas postulita por funkcii dece. + import: + description: '%{link} viajn fluojn de Google Reader kaj importi ilin.' + export: Eksporti + fields: + import: Importi + not_now: Ne nun + subtitle: Ek agordas viajn fluojn. + title: Bonvenon surŝipe. + layout: + back_to_work: Revenu al laboro, malviglanto! + export: Eksporti + hey: He! + import: Importi + logout: Elsaluti + support: Subteno + title: stringer | via RSS amiko + partials: + action_bar: + add_feed: Aldoni fluo + archived_stories: Arĥivitaj rakontoj + mark_all: Marki ĉiu kiel legita + refresh: Aktualigi + starred_stories: Stelitaj rakontoj + view_feeds: Montri fluojn + feed: + last_fetched: + never: Neniam + last_updated: Pastine ĝisdatigita + status_bubble: + green: Sukcesa! + red: Eraro okazis dum sintakse analizado. Neniam funkcias antaŭ. + yellow: Eraro okazis dum sintaskse analizado, probable malpermanenta. + feed_action_bar: + add_feed: Aldoni fluon + archived_stories: Arĥivitaj rakontoj + feeds: Montru fluojn + home: Alreveni rakontojn + starred_stories: Stelitaj rakontoj + shortcuts: + keys: + a: Aldoni fluon + f: Iri paĝon de fluoj + jk: Sekva/antaŭa rakonto + left: Antaŭa paĝo + m: Marki ero kiel legita/mallegita + np: Movi (mal)supren + oenter: Baskuligi (mal)fermecon de rakonto + or: aŭ + r: Aktualigi + right: Sekva paĝo + s: Marki eron kiel (mal)stelita + shifta: Marki ciŭ kiel legita + v: Iri URL de rakonto + title: 'Fulmoklavoj ' + zen: + archive: Montri ĉiajn erojn + go_make: Iru fari ion! + gtfo: Ĉesu legadon de blogoj kaj + rss_zero: Vi atingis RSS-nul™ + sessions: + destroy: + flash: + logged_out_successfully: Vi elsalutis. + new: + fields: + password: Pasvorto + submit: Ensaluti + flash: + wrong_password: Malĝusta pasvorto. Reprovu. + rss: RSS + subtitle: Bonrevenon, amiko. + title: Stringer parolas + starred: + next: Sekva + of: de + previous: Antaŭa + sorry: Bedaŭrinde, vi ankoraŭ ne stelis rakontojn. + stories: + keep_unread: Preservi mallegita + time: + formats: + default: '%d %b, %H:%M' + tutorial: + add_feed: Aldoni fluon + as_read: kiel legita + click_to_read: (klaku legi) + description: Ni ekhavas nuntempe kelkajn rakontojn legi, bonvolu atendi. + heroku_hourly_task: Vi bezonas aldoni hora tasko kontroli novajn fluojn. + heroku_one_more_thing: Unu pli aro... + heroku_scheduler: Iru Heroku kaj aldonu tiun horaran taskon + mark_all: Marki ĉia + ready: Okej, estas preta! + refresh: aktualigi + simple: simpla + start: Komencu legado! + subtitle: Tiu estas la instrukcia manlibro. + title: Stringer estas + your_feeds: viaj fluoj + your_stories: viaj rakontoj diff --git a/config/locales/es.yml b/config/locales/es.yml index 93854e54a..d8259c592 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -32,11 +32,11 @@ es: title: ¿Necesitas nuevas historias? edit: fields: - feed_name: - feed_url: - submit: + feed_name: Nombre fuente + feed_url: Fuente URL + submit: Guardar flash: - updated_successfully: + updated_successfully: la fuente ha sido actualizada para ti! index: add: agregar add_some_feeds: Oye, deberias %{add} algunas feeds. @@ -96,7 +96,7 @@ es: shortcuts: keys: a: Añadir una feed - f: + f: Vaya a la pagina de las fuentes jk: Siguiente/previa historia left: Página anterior m: Marcar item como leído/no leído @@ -142,9 +142,9 @@ es: as_read: como leído click_to_read: (haz click para leer) description: Estamos consiguiendo unas historias para leer, danos un moment. - heroku_hourly_task: - heroku_one_more_thing: - heroku_scheduler: + heroku_hourly_task: Debe añadir una hora de trabajo para poder buscar nuevas historias + heroku_one_more_thing: Una cosa mas... + heroku_scheduler: Vaya al Programador Heroku y añada esta tarea mark_all: Marcar todas ready: ¡Bueno esta listo! refresh: refrescar diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 3ec0beade..b4f694c57 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -32,11 +32,11 @@ ja: title: 新しいストーリーが必要ですか? edit: fields: - feed_name: - feed_url: - submit: + feed_name: フィード名 + feed_url: フィードURL + submit: 保存 flash: - updated_successfully: + updated_successfully: フィードが更新されました! index: add: 追加 add_some_feeds: 何かフィードを%{add}する必要があります @@ -78,7 +78,7 @@ ja: mark_all: すべて既読にする refresh: リフレッシュ starred_stories: スター付きストーリー - view_feeds: 購読フィードを見る + view_feeds: フィード一覧 feed: last_fetched: never: まだない @@ -90,13 +90,13 @@ ja: feed_action_bar: add_feed: フィードを追加 archived_stories: アーカイブ - feeds: 購読フィードを見る + feeds: フィード一覧 home: ストーリーに戻る starred_stories: スター付きストーリー shortcuts: keys: a: フィードを追加 - f: + f: フィード一覧ページ jk: 次/前のストーリー left: 前ページ m: 既読/未読切り替え @@ -142,9 +142,9 @@ ja: as_read: as read click_to_read: (click to read) description: あなたのストーリーを読み込んでます、しばらくお待ち下さい - heroku_hourly_task: - heroku_one_more_thing: - heroku_scheduler: + heroku_hourly_task: 新しいストーリーをチェックする毎時タスクを追加する必要があります + heroku_one_more_thing: もうひとつ… + heroku_scheduler: Heroku Scheduler ページに行き、このタスクを追加する mark_all: mark all ready: 準備OK! refresh: refresh diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 78de1693c..b42ecdbe1 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -21,22 +21,22 @@ nl: - dec feeds: add: - description: Plak de URL van de feed die je wilt lezen. + description: Plak de URL van de blog die je wilt lezen. fields: feed_url: Feed-URL submit: Toevoegen flash: - added_successfully: We hebben je nieuwe feed toegevoegd, kijk over een tijdje nog eens. + added_successfully: We hebben je nieuwe feed toegevoegd. Kijk over een tijdje nog eens. already_subscribed_error: Je bent al geabonneerd op deze feed... feed_not_found_error: Die feed konden we niet vinden. Probeer het opnieuw. title: Nieuwe artikelen nodig? edit: fields: - feed_name: - feed_url: - submit: + feed_name: Feednaam + feed_url: Feed-URL + submit: Opslaan flash: - updated_successfully: + updated_successfully: De feed is voor je bijgewerkt! index: add: toevoegen add_some_feeds: Hé, je zou eens wat feeds kunnen %{add}. @@ -64,16 +64,16 @@ nl: subtitle: Laten we je feeds instellen. title: Welkom aan boord. layout: - back_to_work: Aan het werk, luilak! + back_to_work: Ga weer aan het werk, luilak! export: Exporteren hey: Hé! import: Importeren logout: Uitloggen - support: Hulp + support: Ondersteuning title: stringer | jouw rss-buddy partials: action_bar: - add_feed: Feed toevoegen + add_feed: Een feed toevoegen archived_stories: Gearchiveerde artikelen mark_all: Alles als gelezen markeren refresh: Vernieuwen @@ -96,7 +96,7 @@ nl: shortcuts: keys: a: Een feed toevoegen - f: + f: Naar de feeds-pagina jk: Volgend/vorig artikel left: Vorige pagina m: Artikel markeren als gelezen/ongelezen @@ -142,9 +142,9 @@ nl: as_read: als gelezen markeren click_to_read: (klik om te lezen) description: We zijn je artikelen aan het ophalen, geef ons even. - heroku_hourly_task: - heroku_one_more_thing: - heroku_scheduler: + heroku_hourly_task: Om nieuwe artikelen binnen te halen is het nodig dat je een taak instelt die elk uur uitgevoerd wordt. + heroku_one_more_thing: Nog een ding... + heroku_scheduler: Ga naar de Heroku Scheduler en voeg deze taak toe mark_all: alles ready: Okay, klaar! refresh: vernieuwen From ec7867b82a855b1d1164202161cfcd623e8a8a63 Mon Sep 17 00:00:00 2001 From: Matt Swanson Date: Fri, 24 Oct 2014 13:33:45 -0400 Subject: [PATCH 0068/1107] Sync with Localeapp --- config/locales/eo.yml | 2 +- config/locales/es.yml | 2 +- config/locales/nl.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/locales/eo.yml b/config/locales/eo.yml index 36b234075..4dd73b787 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -96,6 +96,7 @@ eo: shortcuts: keys: a: Aldoni fluon + bv: Iri URL de rakonto f: Iri paĝon de fluoj jk: Sekva/antaŭa rakonto left: Antaŭa paĝo @@ -107,7 +108,6 @@ eo: right: Sekva paĝo s: Marki eron kiel (mal)stelita shifta: Marki ciŭ kiel legita - v: Iri URL de rakonto title: 'Fulmoklavoj ' zen: archive: Montri ĉiajn erojn diff --git a/config/locales/es.yml b/config/locales/es.yml index 3ee901ef8..6f7a21446 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -97,7 +97,7 @@ es: keys: a: Añadir una feed bv: Ir a URL de historia - f: Vaya a la pagina de las fuentes + f: Vaya a la pagina de las fuentes jk: Siguiente/previa historia left: Página anterior m: Marcar item como leído/no leído diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 0ee71dcde..115a908e4 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -97,7 +97,7 @@ nl: keys: a: Een feed toevoegen bv: Open de URL van het artikel - f: Naar de feeds-pagina + f: Naar de feeds-pagina jk: Volgend/vorig artikel left: Vorige pagina m: Artikel markeren als gelezen/ongelezen From 737a2a42b078a600c2f524af94a5b410642a8b9b Mon Sep 17 00:00:00 2001 From: Matt Swanson Date: Mon, 27 Oct 2014 10:57:51 -0400 Subject: [PATCH 0069/1107] Update translations --- config/locales/de.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/config/locales/de.yml b/config/locales/de.yml index d33518a78..bef897ecb 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -32,11 +32,11 @@ de: title: Benötigst du neue Geschichten? edit: fields: - feed_name: - feed_url: - submit: + feed_name: Feed-Name + feed_url: Feed-URL + submit: Speichern flash: - updated_successfully: + updated_successfully: Feed updaten index: add: hinzufügen add_some_feeds: Hey, du solltest ein paar Feeds %{add}. @@ -96,13 +96,13 @@ de: shortcuts: keys: a: Einen Feed hinzufügen - bv: Öffne die URL der Geschichte - f: - jk: Nächste/vorherige Geschichte - left: Vorige Seite - m: Markiere Geschichte als gelesen/ungelesen + bv: Öffne die URL des Artikels + f: Zur Feed-Seite + jk: Nächster/vorheriger Artikel + left: Vorherige Seite + m: Markiere Eintrag als gelesen/ungelesen np: Hoch/runter bewegen - oenter: Klappe Geschichte aus/ein + oenter: Klappe Artikel aus/ein or: oder r: Aktualisieren right: Nächste Seite @@ -142,9 +142,9 @@ de: as_read: als gelesen markieren click_to_read: (klicken um zu lesen) description: Wir besorgen dir Geschichten zum Lesen, gib uns eine Sekunde. - heroku_hourly_task: - heroku_one_more_thing: - heroku_scheduler: + heroku_hourly_task: Du brauchst einen stündlichen Cronjob, um auf neue Artikel zu prüfen + heroku_one_more_thing: Eine kleine Sache noch... + heroku_scheduler: Geh zum Heroku Scheduler und füge folgenden Task hinzu mark_all: alle ready: Okay, es ist bereit! refresh: aktualisieren From 3704a011ce266a945681a79d44d3eeafa025a2ee Mon Sep 17 00:00:00 2001 From: sparker Date: Sun, 2 Nov 2014 04:35:36 -0600 Subject: [PATCH 0070/1107] Sanitize titles on creation --- app/repositories/story_repository.rb | 2 +- ...7_fix_invalid_titles_with_unicode_line_endings.rb | 12 ++++++++++++ spec/repositories/story_repository_spec.rb | 11 ++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index 04872d933..886198f68 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -6,7 +6,7 @@ def self.add(entry, feed) entry.url = normalize_url(entry.url, feed.url) Story.create(feed: feed, - title: entry.title, + title: sanitize(entry.title), permalink: entry.url, body: extract_content(entry), is_read: false, diff --git a/db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb b/db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb new file mode 100644 index 000000000..ca6f83210 --- /dev/null +++ b/db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb @@ -0,0 +1,12 @@ +class FixInvalidTitlesWithUnicodeLineEndings < ActiveRecord::Migration + def up + Story.find_each do |story| + valid_title = story.title.gsub("\u2028", '').gsub("\u2029", '') + story.update_attribute(:title, valid_title) + end + end + + def down + # skip + end +end diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index b810ccc49..0ef446a06 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -10,11 +10,20 @@ end it 'normalizes story urls' do - entry = double(url: '//blog.golang.org/context', content: '').as_null_object + entry = double(url: '//blog.golang.org/context', title: '', content: '').as_null_object StoryRepository.should receive(:normalize_url).with(entry.url, feed.url) StoryRepository.add(entry, feed) end + + it "sanitizes titles" do + entry = double(title: "n\u2028\u2029", content: '').as_null_object + StoryRepository.stub(:normalize_url) + + Story.should receive(:create).with(hash_including(title: "n")) + + StoryRepository.add(entry, feed) + end end describe ".expand_absolute_urls" do From 872c1faa6afc10f4064ebfd4df8ece887744ab6b Mon Sep 17 00:00:00 2001 From: Dan Dorman Date: Sat, 27 Dec 2014 17:25:56 -0700 Subject: [PATCH 0071/1107] Add invisible username fields to create and login Functionality is unchanged (the user only sees the password field), but the invisible username field coerces Safari into prompting to save the password. --- app/public/css/styles.css | 4 ++++ app/views/first_run/password.erb | 5 +++++ app/views/sessions/new.erb | 5 +++++ config/locales/en.yml | 6 ++++++ 4 files changed, 20 insertions(+) diff --git a/app/public/css/styles.css b/app/public/css/styles.css index 53d5f716a..bca800ef9 100644 --- a/app/public/css/styles.css +++ b/app/public/css/styles.css @@ -25,6 +25,10 @@ code { margin: 0 auto; } +.hidden { + display: none; +} + /* Wrapper for page content to push down footer */ #wrap { min-height: 100%; diff --git a/app/views/first_run/password.erb b/app/views/first_run/password.erb index 16879d77f..2bd44fc1f 100644 --- a/app/views/first_run/password.erb +++ b/app/views/first_run/password.erb @@ -5,6 +5,11 @@

    <%= t('first_run.password.description') %>


    +
    diff --git a/app/views/sessions/new.erb b/app/views/sessions/new.erb index 16300682b..4daefb2d4 100644 --- a/app/views/sessions/new.erb +++ b/app/views/sessions/new.erb @@ -5,6 +5,11 @@
    +
    diff --git a/config/locales/en.yml b/config/locales/en.yml index ee60d8bfe..22ad2838c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -48,8 +48,11 @@ en: next: Next password: Password password_confirmation: Confirm + username: User name flash: passwords_dont_match: Hey, your password confirmation didn't match. Try again. + help: + username: Please do not change the user name. subtitle: 'There is only one user: you.' title: Stringer is flash: @@ -122,8 +125,11 @@ en: fields: password: Password submit: Login + username: User name flash: wrong_password: That's the wrong password. Try again. + help: + username: Please do not change the user name. rss: RSS subtitle: Welcome back, friend. title: Stringer speaks From 272d48592adc63c4af8a09420b9aa94674742225 Mon Sep 17 00:00:00 2001 From: Dan Dorman Date: Mon, 29 Dec 2014 10:30:32 -0700 Subject: [PATCH 0072/1107] Revise hidden username field to use minimal markup Also modify the CSS for `.hidden` to append `!important`. It actually doesn't matter for where it's currently used, but if a `type="text"` attribute is added to the hidden field, it is superceded by the `display: inline-block` rule of the base Bootstrap CSS. --- app/public/css/styles.css | 4 ++-- app/views/first_run/password.erb | 6 +----- app/views/sessions/new.erb | 6 +----- config/locales/en.yml | 6 ------ 4 files changed, 4 insertions(+), 18 deletions(-) diff --git a/app/public/css/styles.css b/app/public/css/styles.css index bca800ef9..79535fcc7 100644 --- a/app/public/css/styles.css +++ b/app/public/css/styles.css @@ -26,7 +26,7 @@ code { } .hidden { - display: none; + display: none !important; } /* Wrapper for page content to push down footer */ @@ -238,7 +238,7 @@ li.story.cursor { border: 3px solid #484948; } -li.story .story-body-container { +li.story .story-body-container { display: none; } diff --git a/app/views/first_run/password.erb b/app/views/first_run/password.erb index 2bd44fc1f..0c22fce03 100644 --- a/app/views/first_run/password.erb +++ b/app/views/first_run/password.erb @@ -5,11 +5,7 @@

    <%= t('first_run.password.description') %>


    - +
    diff --git a/app/views/sessions/new.erb b/app/views/sessions/new.erb index 4daefb2d4..5c55438a7 100644 --- a/app/views/sessions/new.erb +++ b/app/views/sessions/new.erb @@ -5,11 +5,7 @@
    - +
    diff --git a/config/locales/en.yml b/config/locales/en.yml index 22ad2838c..ee60d8bfe 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -48,11 +48,8 @@ en: next: Next password: Password password_confirmation: Confirm - username: User name flash: passwords_dont_match: Hey, your password confirmation didn't match. Try again. - help: - username: Please do not change the user name. subtitle: 'There is only one user: you.' title: Stringer is flash: @@ -125,11 +122,8 @@ en: fields: password: Password submit: Login - username: User name flash: wrong_password: That's the wrong password. Try again. - help: - username: Please do not change the user name. rss: RSS subtitle: Welcome back, friend. title: Stringer speaks From 5e3dc08721c49b918c4ec5d598b7535d6723ede7 Mon Sep 17 00:00:00 2001 From: Anandu B Ajith Date: Wed, 7 Jan 2015 15:02:18 +0530 Subject: [PATCH 0073/1107] Adding password reset --- docs/OpenShift.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/OpenShift.md b/docs/OpenShift.md index fd07d673a..6b2c58792 100644 --- a/docs/OpenShift.md +++ b/docs/OpenShift.md @@ -116,3 +116,12 @@ After importing feeds, a cron job is needed on OpenShift to fetch feeds. ``` 5. Done! The cron job should fetch feeds every hour. + +Password Reset +-------------- +In the event that you need to change your password, run the following commands +``` +rhc ssh app-name +cd app-root/repo +bundle exec rake change_password RACK_ENV="production" +``` From cd74e52b4f446b52f1f6294c64ec0f0de87190bf Mon Sep 17 00:00:00 2001 From: Anandu B Ajith Date: Wed, 7 Jan 2015 15:16:49 +0530 Subject: [PATCH 0074/1107] change app-name to feeds --- docs/OpenShift.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/OpenShift.md b/docs/OpenShift.md index 6b2c58792..26524b9b2 100644 --- a/docs/OpenShift.md +++ b/docs/OpenShift.md @@ -121,7 +121,7 @@ Password Reset -------------- In the event that you need to change your password, run the following commands ``` -rhc ssh app-name +rhc ssh feeds cd app-root/repo bundle exec rake change_password RACK_ENV="production" ``` From d9e128f633736e1665cb33690226c3b191f9967f Mon Sep 17 00:00:00 2001 From: matt swanson Date: Wed, 14 Jan 2015 21:13:03 -0500 Subject: [PATCH 0075/1107] Switch badge styles --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cc733442d..8510bb934 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Stringer -[![Build Status](http://img.shields.io/travis/swanson/stringer.svg)](https://travis-ci.org/swanson/stringer) -[![Code Climate](http://img.shields.io/codeclimate/github/swanson/stringer.svg)](https://codeclimate.com/github/swanson/stringer) -[![Coverage Status](http://img.shields.io/coveralls/swanson/stringer.svg)](https://coveralls.io/r/swanson/stringer) +[![Build Status](http://img.shields.io/travis/swanson/stringer.svg?style=flat)](https://travis-ci.org/swanson/stringer) +[![Code Climate](http://img.shields.io/codeclimate/github/swanson/stringer.svg?style=flat)](https://codeclimate.com/github/swanson/stringer) +[![Coverage Status](http://img.shields.io/coveralls/swanson/stringer.svg?style=flat)](https://coveralls.io/r/swanson/stringer) ### A self-hosted, anti-social RSS reader. From 247f6fb854f79971b9199a751734cbe4bc5a7866 Mon Sep 17 00:00:00 2001 From: milligramme Date: Wed, 28 Jan 2015 10:25:49 +0900 Subject: [PATCH 0076/1107] set APP_URL for heroku when I created new app on heroku and set APP_URL, got a following error. ``` ! Usage: heroku config:set KEY1=VALUE1 [KEY2=VALUE2 ...] ! Must specify KEY and VALUE to set. ``` `heroku apps:info` recently give me two urls, Git URL and Web URL. ex. Git URL: https://git.heroku.com/TESTAPP-cedar-14.git Web URL: https://TESTAPP-cedar-14.herokuapp.com/ to set APP_URL I modified regexp to get only Web URL. is there more good way? --- docs/Heroku.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Heroku.md b/docs/Heroku.md index f14600b10..714b90efa 100644 --- a/docs/Heroku.md +++ b/docs/Heroku.md @@ -4,7 +4,7 @@ cd stringer heroku create git push heroku master -heroku config:set APP_URL=`heroku apps:info | grep -o 'http[^"]*'` +heroku config:set APP_URL=`heroku apps:info | grep -o 'http[^"]*/$'` heroku config:set SECRET_TOKEN=`openssl rand -hex 20` heroku run rake db:migrate @@ -33,4 +33,4 @@ heroku restart ## Password Reset -In the event that you need to change your password, run `heroku run rake change_password` from the app folder. \ No newline at end of file +In the event that you need to change your password, run `heroku run rake change_password` from the app folder. From aff8aa858edc0b1bb9dff9f381eacc7078c419a3 Mon Sep 17 00:00:00 2001 From: milligramme Date: Thu, 29 Jan 2015 10:20:04 +0900 Subject: [PATCH 0077/1107] get web_url by using --shell opt and cut command --- docs/Heroku.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Heroku.md b/docs/Heroku.md index 714b90efa..0462a9630 100644 --- a/docs/Heroku.md +++ b/docs/Heroku.md @@ -4,7 +4,7 @@ cd stringer heroku create git push heroku master -heroku config:set APP_URL=`heroku apps:info | grep -o 'http[^"]*/$'` +heroku config:set APP_URL=`heroku apps:info --shell | grep web_url | cut -d= -f2` heroku config:set SECRET_TOKEN=`openssl rand -hex 20` heroku run rake db:migrate From 81ac1fe9f630cd0509ccf28e5b8ebc0db94618ee Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Thu, 12 Feb 2015 21:31:18 +0100 Subject: [PATCH 0078/1107] Updates curb to v0.8.6 This adds compatibility for ruby 2.2.0 while retaining compatibility with 2.0.x and 2.1.x. More info: https://github.com/taf2/curb/pull/197 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index cdd801146..6796748af 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -38,7 +38,7 @@ GEM simplecov (>= 0.7) term-ansicolor thor - curb (0.8.5) + curb (0.8.6) debugger-linecache (1.2.0) delayed_job (4.0.0) activesupport (>= 3.0, < 4.1) From 0cc92be3d2e5c84738373808badc441f4e76b8dc Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Thu, 12 Feb 2015 21:32:14 +0100 Subject: [PATCH 0079/1107] Updates sqlite3 to v1.3.10 This adds compatibility for ruby 2.2.0 while retaining compatibility with v2.0.x and v2.1.x. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6796748af..2b3c50f96 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -132,7 +132,7 @@ GEM sinatra-flash (0.3.0) sinatra (>= 1.0.0) slop (3.4.7) - sqlite3 (1.3.8) + sqlite3 (1.3.10) term-ansicolor (1.2.2) tins (~> 0.8) thor (0.18.1) From 7506ec468e2723331cb060903df7c9702d11caac Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Thu, 12 Feb 2015 21:34:23 +0100 Subject: [PATCH 0080/1107] Updates kgio to v2.9.3 This adds compatibility for ruby 2.2.0 while retaining compatibility with 2.0.x and 2.1.x. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2b3c50f96..763736e71 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -58,7 +58,7 @@ GEM hpricot (0.8.6) i18n (0.6.9) jsmin (1.0.1) - kgio (2.8.1) + kgio (2.9.3) loofah (2.0.0) nokogiri (>= 1.5.9) method_source (0.8.2) From 9573fe9b7f90a4d9ae23133ab653df34c25db494 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Thu, 12 Feb 2015 22:07:27 +0100 Subject: [PATCH 0081/1107] Updates raindrops to v1.3.0 This adds compatibility for ruby 2.2.0 while retaining compatibility with 2.0.x and 2.1.x. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 763736e71..2283fa967 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -86,7 +86,7 @@ GEM racksh (1.0.0) rack (>= 1.0) rack-test (>= 0.5) - raindrops (0.12.0) + raindrops (0.13.0) rake (10.1.1) rest-client (1.6.7) mime-types (>= 1.16) From 0dbc86e2c11c20a08db538cd6efaf8e91457f9a6 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Thu, 12 Feb 2015 21:51:35 +0100 Subject: [PATCH 0082/1107] Adds ruby 2.2.0 to Travis build matrix --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 61ce87bb1..23c8af62d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,8 @@ language: ruby rvm: - 2.0.0 - 2.1.0 -before_install: + - 2.2.0 +before_install: - gem update bundler - sed -i '1d' Gemfile before_script: @@ -15,3 +16,4 @@ script: matrix: allow_failures: - rvm: 2.1.0 + - rvm: 2.2.0 From f89c142306b9a2ea31acebdb952ab3384aadff77 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Thu, 5 Mar 2015 19:20:54 +0100 Subject: [PATCH 0083/1107] Enable PostgreSQL addon on one-click deploy Stringer depends on a PostgreSQL database, so enable the hobby-dev (free) version of the addon on one-click deploy. Ref: #368 --- app.json | 1 + 1 file changed, 1 insertion(+) diff --git a/app.json b/app.json index d8ca2de19..2b2b22555 100644 --- a/app.json +++ b/app.json @@ -22,6 +22,7 @@ } }, "addons": [ + "heroku-postgresql:hobby-dev", "scheduler:standard" ] } From d8710e11816862ece73dc925a5929e9bf2a08ba9 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Fri, 27 Mar 2015 21:24:29 +0100 Subject: [PATCH 0084/1107] Updates activerecord to v4.0.13 This fixes an issue with #destroy on Ruby 2.2 and fixes various warnings. --- Gemfile.lock | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2283fa967..c73d2b1b2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,23 +1,22 @@ GEM remote: https://rubygems.org/ specs: - activemodel (4.0.2) - activesupport (= 4.0.2) + activemodel (4.0.13) + activesupport (= 4.0.13) builder (~> 3.1.0) - activerecord (4.0.2) - activemodel (= 4.0.2) + activerecord (4.0.13) + activemodel (= 4.0.13) activerecord-deprecated_finders (~> 1.0.2) - activesupport (= 4.0.2) + activesupport (= 4.0.13) arel (~> 4.0.0) activerecord-deprecated_finders (1.0.3) - activesupport (4.0.2) - i18n (~> 0.6, >= 0.6.4) + activesupport (4.0.13) + i18n (~> 0.6, >= 0.6.9) minitest (~> 4.2) multi_json (~> 1.3) thread_safe (~> 0.1) tzinfo (~> 0.3.37) arel (4.0.2) - atomic (1.1.14) backports (3.6.1) bcrypt-ruby (3.1.2) builder (3.1.4) @@ -56,7 +55,7 @@ GEM loofah (~> 2.0.0) sax-machine (~> 0.2.1) hpricot (0.8.6) - i18n (0.6.9) + i18n (0.6.11) jsmin (1.0.1) kgio (2.9.3) loofah (2.0.0) @@ -65,7 +64,7 @@ GEM mime-types (2.0) mini_portile (0.5.2) minitest (4.7.5) - multi_json (1.10.1) + multi_json (1.11.0) nokogiri (1.6.1) mini_portile (~> 0.5.0) pg (0.17.1) @@ -137,12 +136,11 @@ GEM tins (~> 0.8) thor (0.18.1) thread (0.1.3) - thread_safe (0.1.3) - atomic + thread_safe (0.3.5) tilt (1.4.1) timecop (0.7.1) tins (0.13.1) - tzinfo (0.3.38) + tzinfo (0.3.43) unicorn (4.7.0) kgio (~> 2.6) rack From 7630f6c730f01f1f60173f73c8ff523e3cea543d Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Fri, 27 Mar 2015 22:10:38 +0100 Subject: [PATCH 0085/1107] Sync schema 3704a011ce266a945681a79d44d3eeafa025a2ee added a migration but didn't update the schema. --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 18ff40d37..3f7ef303d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140421224454) do +ActiveRecord::Schema.define(version: 20141102103617) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From 04133bc5569bb465fd7ba335cc6a0a316c2060b8 Mon Sep 17 00:00:00 2001 From: Bodo Tasche Date: Wed, 8 Apr 2015 20:45:51 +0200 Subject: [PATCH 0086/1107] Do not fix the title if the title is nil, fixes #373 --- ...02103617_fix_invalid_titles_with_unicode_line_endings.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb b/db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb index ca6f83210..e10762619 100644 --- a/db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb +++ b/db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb @@ -1,8 +1,10 @@ class FixInvalidTitlesWithUnicodeLineEndings < ActiveRecord::Migration def up Story.find_each do |story| - valid_title = story.title.gsub("\u2028", '').gsub("\u2029", '') - story.update_attribute(:title, valid_title) + unless story.title.nil? + valid_title = story.title.gsub("\u2028", '').gsub("\u2029", '') + story.update_attribute(:title, valid_title) + end end end From 843112b17676fb98ea03153c65a04432b9a43f51 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Thu, 30 Apr 2015 20:01:06 +0200 Subject: [PATCH 0087/1107] Fill feed URL field from params Read the feed URL from params on request to `/feeds/new` and let it fill the feed URL field. This enables integration with the [RSS Subscription Extension][rss-subscription-ext] extension for Google Chrome and should allow integration with similar extensions, for Google Chrome or other browsers. [rss-subscription-ext]: https://chrome.google.com/webstore/detail/rss-subscription-extensio/nlbjncdgjeocebhnmkbbbdekmmmcbfjd --- app/controllers/feeds_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/feeds_controller.rb b/app/controllers/feeds_controller.rb index 1b84c0806..460ca3756 100644 --- a/app/controllers/feeds_controller.rb +++ b/app/controllers/feeds_controller.rb @@ -31,6 +31,7 @@ class Stringer < Sinatra::Base end get "/feeds/new" do + @feed_url = params[:feed_url] erb :'feeds/add' end From a128047fea7b0ceed3309c2e6bc8db9c5abd5aea Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Thu, 30 Apr 2015 20:39:04 +0200 Subject: [PATCH 0088/1107] Sort keys in .travis.yml lexicographically To allow for easier scanning. --- .travis.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 23c8af62d..56507ecbd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,3 @@ -language: ruby -rvm: - - 2.0.0 - - 2.1.0 - - 2.2.0 before_install: - gem update bundler - sed -i '1d' Gemfile @@ -10,10 +5,15 @@ before_script: - npm install -g mocha-phantomjs@2.0.2 - bundle exec rake test_js &> /dev/null & - sleep 5 -script: - - bundle exec rspec - - mocha-phantomjs http://localhost:4567/test +language: ruby matrix: allow_failures: - rvm: 2.1.0 - rvm: 2.2.0 +rvm: + - 2.0.0 + - 2.1.0 + - 2.2.0 +script: + - bundle exec rspec + - mocha-phantomjs http://localhost:4567/test From d82c735a66118f1cd560eab4890b9612f96c6cff Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Thu, 30 Apr 2015 20:40:56 +0200 Subject: [PATCH 0089/1107] Use Travis CI container-based infrastructure From : > Jobs running on container-based infrastructure: > > 1. start up faster > 2. allow the use of caches for public repositories > 3. disallow the use of sudo, setuid and setgid executables --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 56507ecbd..808765150 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,3 +17,4 @@ rvm: script: - bundle exec rspec - mocha-phantomjs http://localhost:4567/test +sudo: false From eaf4c7649621fe5bff3ea3f8f40004e8f7d9e885 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Thu, 30 Apr 2015 21:03:26 +0200 Subject: [PATCH 0090/1107] Enable Bundler caching Explicitly enable Bundler caching until it is turned on by default. See . --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 808765150..db74816c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ before_script: - npm install -g mocha-phantomjs@2.0.2 - bundle exec rake test_js &> /dev/null & - sleep 5 +cache: bundler language: ruby matrix: allow_failures: From 26ff3361f710bfdecc00bd0cddc02ba1332706bc Mon Sep 17 00:00:00 2001 From: Jacob Krall Date: Wed, 12 Aug 2015 15:22:33 -0400 Subject: [PATCH 0091/1107] use TLS for external resources --- README.md | 6 +++--- app/utils/sample_story.rb | 4 ++-- app/views/tutorial.erb | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8510bb934..bf893bcc8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Stringer -[![Build Status](http://img.shields.io/travis/swanson/stringer.svg?style=flat)](https://travis-ci.org/swanson/stringer) -[![Code Climate](http://img.shields.io/codeclimate/github/swanson/stringer.svg?style=flat)](https://codeclimate.com/github/swanson/stringer) -[![Coverage Status](http://img.shields.io/coveralls/swanson/stringer.svg?style=flat)](https://coveralls.io/r/swanson/stringer) +[![Build Status](https://img.shields.io/travis/swanson/stringer.svg?style=flat)](https://travis-ci.org/swanson/stringer) +[![Code Climate](https://img.shields.io/codeclimate/github/swanson/stringer.svg?style=flat)](https://codeclimate.com/github/swanson/stringer) +[![Coverage Status](https://img.shields.io/coveralls/swanson/stringer.svg?style=flat)](https://coveralls.io/r/swanson/stringer) ### A self-hosted, anti-social RSS reader. diff --git a/app/utils/sample_story.rb b/app/utils/sample_story.rb index 9b98de31d..dafbe0468 100644 --- a/app/utils/sample_story.rb +++ b/app/utils/sample_story.rb @@ -12,7 +12,7 @@ def body tattooed. Keffiyeh mumblecore fingerstache, sartorial sriracha disrupt biodiesel cred. Skateboard yr cosby sweater, narwhal beard ethnic jean shorts aesthetic. Post-ironic flannel mlkshk, pickled VHS wolf banjo forage portland wayfarers.

    - +

    Selfies mumblecore odd future irony DIY messenger bag. Authentic neutra next level selvage squid. Four loko freegan occupy, tousled vinyl leggings selvage messenger bag. Four loko wayfarers kale chips, next level banksy banh mi umami flannel hella. @@ -43,4 +43,4 @@ def as_json(options = {}) keep_unread: keep_unread } end -end \ No newline at end of file +end diff --git a/app/views/tutorial.erb b/app/views/tutorial.erb index 8364f7b8b..b7d76ecba 100644 --- a/app/views/tutorial.erb +++ b/app/views/tutorial.erb @@ -1,5 +1,5 @@ <% content_for :head do %> - + <% end %>

    From 9cd6f9fbdc46ab46f0c89a60b9f8cb35b11bca16 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Mon, 24 Aug 2015 19:38:48 +0200 Subject: [PATCH 0092/1107] Enforce SSL by default when using the Heroku button > Heroku serves a wildcard *.herokuapp.com certificate, so for most people, HSTS > should be enabled. --- app.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app.json b/app.json index 2b2b22555..fb4ed7149 100644 --- a/app.json +++ b/app.json @@ -19,6 +19,10 @@ "LOCALE": { "description": "Specify the translation locale you wish to use", "value": "en" + }, + "ENFORCE_SSL": { + "description": "Force all clients to connect over SSL", + "value": "true" } }, "addons": [ From a52f4549ac1345db78544503ae46c32b735bd81d Mon Sep 17 00:00:00 2001 From: milligramme Date: Wed, 30 Sep 2015 15:11:29 +0900 Subject: [PATCH 0093/1107] Fixed CI badges --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bf893bcc8..7d44a64d0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Stringer -[![Build Status](https://img.shields.io/travis/swanson/stringer.svg?style=flat)](https://travis-ci.org/swanson/stringer) -[![Code Climate](https://img.shields.io/codeclimate/github/swanson/stringer.svg?style=flat)](https://codeclimate.com/github/swanson/stringer) -[![Coverage Status](https://img.shields.io/coveralls/swanson/stringer.svg?style=flat)](https://coveralls.io/r/swanson/stringer) +[![Build Status](https://api.travis-ci.org/swanson/stringer.svg?style=flat)](https://travis-ci.org/swanson/stringer) +[![Code Climate](https://codeclimate.com/github/swanson/stringer.svg?style=flat)](https://codeclimate.com/github/swanson/stringer) +[![Coverage Status](https://coveralls.io/repos/swanson/stringer/badge.svg?style=flat)](https://coveralls.io/r/swanson/stringer) ### A self-hosted, anti-social RSS reader. From 790f2d6696eb8d3af2f9102c96143e4cde601548 Mon Sep 17 00:00:00 2001 From: Aliou Diallo Date: Mon, 5 Oct 2015 11:37:05 +0200 Subject: [PATCH 0094/1107] Redirect to the previous page after login. --- app.rb | 1 + app/controllers/sessions_controller.rb | 8 ++++++-- spec/controllers/sessions_controller_spec.rb | 10 ++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app.rb b/app.rb index 4eacb05a9..d26a7c95d 100644 --- a/app.rb +++ b/app.rb @@ -91,6 +91,7 @@ def t(*args) I18n.locale = ENV["LOCALE"].blank? ? :en : ENV["LOCALE"].to_sym if !is_authenticated? && needs_authentication?(request.path) + session[:redirect_to] = request.path redirect '/login' end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index cd3a5b0f7..e956c907d 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -10,7 +10,11 @@ class Stringer < Sinatra::Base if user session[:user_id] = user.id - redirect to("/") + if session[:redirect_to].present? + redirect to(session.delete(:redirect_to)) + else + redirect to("/") + end else flash.now[:error] = t('sessions.new.flash.wrong_password') erb :"sessions/new" @@ -23,4 +27,4 @@ class Stringer < Sinatra::Base redirect to("/") end -end \ No newline at end of file +end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 5521030e0..342cd93dc 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -33,6 +33,16 @@ last_response.status.should be 302 URI::parse(last_response.location).path.should eq "/" end + + it "redirects to the previous path when present" do + SignInUser.stub(:sign_in).and_return(double(id: 1)) + + post "/login", { password: "the-password" }, + 'rack.session' => { redirect_to: '/archive' } + + session[:redirect_to].should be_nil + URI::parse(last_response.location).path.should eq "/archive" + end end describe "GET /logout" do From 11a6d8791e739d7f686bd8cfa97584d389aca6ae Mon Sep 17 00:00:00 2001 From: Aliou Diallo Date: Mon, 5 Oct 2015 21:09:38 +0200 Subject: [PATCH 0095/1107] Specify a default redirection path. --- app/controllers/sessions_controller.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index e956c907d..b04d029df 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -10,11 +10,8 @@ class Stringer < Sinatra::Base if user session[:user_id] = user.id - if session[:redirect_to].present? - redirect to(session.delete(:redirect_to)) - else - redirect to("/") - end + redirect_uri = session.delete(:redirect_to) || '/' + redirect to(redirect_uri) else flash.now[:error] = t('sessions.new.flash.wrong_password') erb :"sessions/new" From 0bc077ad9dfd6e9f51d68e570deb333fe4eb9d57 Mon Sep 17 00:00:00 2001 From: Pascal Widdershoven Date: Sun, 8 Nov 2015 12:17:24 +0100 Subject: [PATCH 0096/1107] Fix handling of relative entry urls Before, normalize_url would completely bork relative urls. In the example used in the spec the result would be `https:/progrium/dokku/releases/tag/v0.4.4` which is of course completely wrong. --- app/repositories/story_repository.rb | 6 ++++-- spec/repositories/story_repository_spec.rb | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index 886198f68..a46ca38b8 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -124,8 +124,10 @@ def self.normalize_url(url, base_url) uri = URI.parse(url) base_uri = URI.parse(base_url) - unless uri.scheme - uri.scheme = base_uri.scheme || 'http' + # resolve (protocol) relative URIs + if uri.relative? + scheme = base_uri.scheme || 'http' + uri = URI.join("#{scheme}://#{base_uri.host}", uri) end uri.to_s diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 0ef446a06..35320dcb9 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -153,5 +153,10 @@ url = StoryRepository.normalize_url("//blog.golang.org/context", "//blog.golang.org/feed.atom") url.should eq 'http://blog.golang.org/context' end + + it "resolves relative urls" do + url = StoryRepository.normalize_url("/progrium/dokku/releases/tag/v0.4.4", "https://github.com/progrium/dokku/releases.atom") + url.should eq "https://github.com/progrium/dokku/releases/tag/v0.4.4" + end end end From 9971c82c0ce0789f8d419c59ec75cd12ce9ad5b1 Mon Sep 17 00:00:00 2001 From: John Berberich Date: Wed, 2 Dec 2015 09:49:57 -0500 Subject: [PATCH 0097/1107] Fix command that sets APP_URL in Heroku.md The `heroku apps:info --shell` command outputs the app's URL in an attribute named `web-url`, not `web_url` (with a hyphen, not an underscore). --- docs/Heroku.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Heroku.md b/docs/Heroku.md index 0462a9630..796cebb88 100644 --- a/docs/Heroku.md +++ b/docs/Heroku.md @@ -4,7 +4,7 @@ cd stringer heroku create git push heroku master -heroku config:set APP_URL=`heroku apps:info --shell | grep web_url | cut -d= -f2` +heroku config:set APP_URL=`heroku apps:info --shell | grep web-url | cut -d= -f2` heroku config:set SECRET_TOKEN=`openssl rand -hex 20` heroku run rake db:migrate From ef26cb2090adac0e10e4078c212b915db0b6a3e8 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Mon, 7 Dec 2015 21:07:46 +0100 Subject: [PATCH 0098/1107] Adds initial rubocop config and adds all offences to a todo list --- .rubocop.yml | 6 + .rubocop_todo.yml | 474 ++++++++++++++++++++++++++++++++++++++++++++++ Gemfile | 1 + Gemfile.lock | 16 ++ 4 files changed, 497 insertions(+) create mode 100644 .rubocop.yml create mode 100644 .rubocop_todo.yml diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 000000000..cc19aafc0 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,6 @@ +inherit_from: .rubocop_todo.yml + +AllCops: + Exclude: + - 'db/schema.rb' + - 'vendor/**/*' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 000000000..cd8b153c4 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,474 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2015-12-07 21:05:42 +0100 using RuboCop version 0.34.2. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 2 +Lint/HandleExceptions: + Exclude: + - 'Rakefile' + - 'app/repositories/story_repository.rb' + +# Offense count: 2 +Lint/RescueException: + Exclude: + - 'app/tasks/fetch_feed.rb' + - 'app/utils/feed_discovery.rb' + +# Offense count: 1 +Lint/ShadowingOuterLocalVariable: + Exclude: + - 'spec/support/active_record.rb' + +# Offense count: 6 +# Cop supports --auto-correct. +Lint/UnusedBlockArgument: + Exclude: + - 'Rakefile' + - 'config/unicorn.rb' + +# Offense count: 5 +# Cop supports --auto-correct. +Lint/UnusedMethodArgument: + Exclude: + - 'app/fever_api/authentication.rb' + - 'app/models/story.rb' + - 'app/utils/feed_discovery.rb' + - 'app/utils/sample_story.rb' + - 'spec/support/feed_server.rb' + +# Offense count: 1 +Lint/UselessAccessModifier: + Exclude: + - 'app/repositories/feed_repository.rb' + +# Offense count: 5 +Lint/UselessAssignment: + Exclude: + - 'app/tasks/change_password.rb' + - 'spec/repositories/feed_repository_spec.rb' + +# Offense count: 5 +Metrics/AbcSize: + Max: 37 + +# Offense count: 1 +# Configuration parameters: CountComments. +Metrics/ClassLength: + Max: 108 + +# Offense count: 2 +Metrics/CyclomaticComplexity: + Max: 9 + +# Offense count: 137 +# Configuration parameters: AllowURI, URISchemes. +Metrics/LineLength: + Max: 352 + +# Offense count: 11 +# Configuration parameters: CountComments. +Metrics/MethodLength: + Max: 22 + +# Offense count: 2 +Metrics/PerceivedComplexity: + Max: 9 + +# Offense count: 4 +# Cop supports --auto-correct. +Performance/StringReplacement: + Exclude: + - 'db/migrate/20140421224454_fix_invalid_unicode.rb' + - 'db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb' + +# Offense count: 14 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle, SupportedLastArgumentHashStyles. +Style/AlignHash: + Exclude: + - 'Rakefile' + - 'app/repositories/story_repository.rb' + - 'spec/repositories/story_repository_spec.rb' + - 'spec/tasks/fetch_feed_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +Style/AlignParameters: + Exclude: + - 'spec/controllers/sessions_controller_spec.rb' + +# Offense count: 3 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +Style/AndOr: + Exclude: + - 'app/controllers/feeds_controller.rb' + - 'app/controllers/first_run_controller.rb' + - 'config/unicorn.rb' + +# Offense count: 7 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, ProceduralMethods, FunctionalMethods, IgnoredMethods. +Style/BlockDelimiters: + Enabled: false + +# Offense count: 4 +# Cop supports --auto-correct. +Style/BlockEndNewline: + Exclude: + - 'spec/commands/feeds/import_from_opml_spec.rb' + +# Offense count: 5 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +Style/BracesAroundHashParameters: + Exclude: + - 'spec/controllers/debug_controller_spec.rb' + - 'spec/controllers/feeds_controller_spec.rb' + - 'spec/controllers/first_run_controller_spec.rb' + - 'spec/fever_api_spec.rb' + +# Offense count: 1 +# Configuration parameters: EnforcedStyle, SupportedStyles. +Style/ClassAndModuleChildren: + Exclude: + - 'fever_api.rb' + +# Offense count: 11 +# Cop supports --auto-correct. +Style/ColonMethodCall: + Exclude: + - 'config/unicorn.rb' + - 'spec/controllers/feeds_controller_spec.rb' + - 'spec/controllers/first_run_controller_spec.rb' + - 'spec/controllers/sessions_controller_spec.rb' + - 'spec/controllers/stories_controller_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Style/CommentIndentation: + Exclude: + - 'app/models/user.rb' + +# Offense count: 76 +# Configuration parameters: Exclude. +Style/Documentation: + Enabled: false + +# Offense count: 3 +Style/DoubleNegation: + Exclude: + - 'app/controllers/stories_controller.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +Style/EmptyElse: + Exclude: + - 'app/commands/users/sign_in_user.rb' + +# Offense count: 8 +# Cop supports --auto-correct. +# Configuration parameters: AllowAdjacentOneLineDefs. +Style/EmptyLineBetweenDefs: + Exclude: + - 'app/utils/sample_story.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Style/EmptyLines: + Exclude: + - 'spec/commands/users/sign_in_user_spec.rb' + +# Offense count: 3 +# Cop supports --auto-correct. +Style/EmptyLinesAroundAccessModifier: + Exclude: + - 'app/controllers/first_run_controller.rb' + - 'app/models/story.rb' + - 'app/tasks/fetch_feed.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +Style/EmptyLinesAroundBlockBody: + Exclude: + - 'app/utils/opml_parser.rb' + - 'db/schema.rb' + - 'spec/models/story_spec.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +Style/EmptyLinesAroundClassBody: + Exclude: + - 'app/tasks/fetch_feed.rb' + - 'spec/javascript/test_controller.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: AllowForAlignment. +Style/ExtraSpacing: + Exclude: + - 'app/commands/stories/mark_feed_as_read.rb' + +# Offense count: 1 +# Configuration parameters: MinBodyLength. +Style/GuardClause: + Exclude: + - 'fever_api.rb' + +# Offense count: 10 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, UseHashRocketsWithSymbolValues. +Style/HashSyntax: + Enabled: false + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: MaxLineLength. +Style/IfUnlessModifier: + Exclude: + - 'app/controllers/first_run_controller.rb' + - 'spec/support/active_record.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +Style/IndentHash: + Enabled: false + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: Width. +Style/IndentationWidth: + Exclude: + - 'spec/tasks/fetch_feed_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Style/LeadingCommentSpace: + Exclude: + - 'db/migrate/20130821020313_update_nil_entry_ids.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +Style/MethodCallParentheses: + Exclude: + - 'spec/tasks/remove_old_stories_spec.rb' + +# Offense count: 1 +Style/MultilineBlockChain: + Exclude: + - 'app/models/migration_status.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +Style/MultilineBlockLayout: + Exclude: + - 'spec/commands/feeds/import_from_opml_spec.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +Style/MultilineOperationIndentation: + Enabled: false + +# Offense count: 1 +# Cop supports --auto-correct. +Style/NegatedIf: + Exclude: + - 'app/helpers/authentication_helpers.rb' + +# Offense count: 2 +# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. +Style/Next: + Exclude: + - 'app/commands/feeds/import_from_opml.rb' + - 'app/repositories/story_repository.rb' + +# Offense count: 11 +# Cop supports --auto-correct. +Style/NumericLiterals: + MinDigits: 15 + +# Offense count: 4 +# Cop supports --auto-correct. +# Configuration parameters: PreferredDelimiters. +Style/PercentLiteralDelimiters: + Exclude: + - 'spec/javascript/test_controller.rb' + - 'spec/repositories/story_repository_spec.rb' + +# Offense count: 4 +# Configuration parameters: NamePrefix, NamePrefixBlacklist. +Style/PredicateName: + Exclude: + - 'app/helpers/authentication_helpers.rb' + - 'app/models/feed.rb' + - 'app/utils/sample_story.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Style/RedundantBegin: + Exclude: + - 'app/tasks/fetch_feed.rb' + +# Offense count: 36 +# Cop supports --auto-correct. +Style/RedundantSelf: + Exclude: + - 'app/models/feed.rb' + - 'app/models/group.rb' + - 'app/models/story.rb' + - 'app/tasks/fetch_feeds.rb' + - 'spec/factories/feed_factory.rb' + - 'spec/factories/group_factory.rb' + - 'spec/factories/story_factory.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes. +Style/RegexpLiteral: + Exclude: + - 'app.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Style/RescueModifier: + Exclude: + - 'app/fever_api/read_items.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +Style/SignalException: + Exclude: + - 'spec/support/active_record.rb' + +# Offense count: 8 +# Cop supports --auto-correct. +# Configuration parameters: AllowIfMethodIsEmpty. +Style/SingleLineMethods: + Exclude: + - 'app/utils/sample_story.rb' + +# Offense count: 29 +# Cop supports --auto-correct. +Style/SingleSpaceBeforeFirstArg: + Exclude: + - 'app.rb' + - 'db/migrate/20130425222157_add_delayed_job.rb' + - 'db/schema.rb' + +# Offense count: 6 +# Cop supports --auto-correct. +Style/SpaceAfterComma: + Exclude: + - 'app/models/story.rb' + - 'app/utils/opml_parser.rb' + - 'spec/fever_api_spec.rb' + - 'spec/tasks/fetch_feed_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +Style/SpaceAroundEqualsInParameterDefault: + Enabled: false + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: MultiSpaceAllowedForOperators. +Style/SpaceAroundOperators: + Exclude: + - 'app.rb' + - 'spec/factories/story_factory.rb' + +# Offense count: 32 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +Style/SpaceBeforeBlockBraces: + Enabled: false + +# Offense count: 1 +# Cop supports --auto-correct. +Style/SpaceBeforeComma: + Exclude: + - 'app/models/feed.rb' + +# Offense count: 18 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. +Style/SpaceInsideBlockBraces: + Enabled: false + +# Offense count: 27 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SupportedStyles. +Style/SpaceInsideHashLiteralBraces: + Enabled: false + +# Offense count: 13 +# Cop supports --auto-correct. +Style/SpaceInsideParens: + Exclude: + - 'spec/commands/feeds/import_from_opml_spec.rb' + - 'spec/fever_api/read_feeds_spec.rb' + - 'spec/fever_api/read_items_spec.rb' + +# Offense count: 960 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +Style/StringLiterals: + Enabled: false + +# Offense count: 2 +Style/StructInheritance: + Exclude: + - 'app/jobs/fetch_feed_job.rb' + - 'app/utils/sample_story.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Style/SymbolLiteral: + Exclude: + - 'app/controllers/debug_controller.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +# Configuration parameters: IgnoredMethods. +Style/SymbolProc: + Exclude: + - 'app/fever_api/read_feeds.rb' + - 'app/fever_api/read_items.rb' + - 'app/fever_api/sync_saved_item_ids.rb' + - 'app/fever_api/sync_unread_item_ids.rb' + +# Offense count: 30 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +Style/TrailingBlankLines: + Enabled: false + +# Offense count: 3 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles. +Style/TrailingComma: + Exclude: + - 'spec/fever_api/read_items_spec.rb' + +# Offense count: 42 +# Cop supports --auto-correct. +Style/TrailingWhitespace: + Enabled: false + +# Offense count: 7 +# Cop supports --auto-correct. +# Configuration parameters: WordRegex. +Style/WordArray: + MinSize: 3 diff --git a/Gemfile b/Gemfile index 2f24ad438..b7546e9db 100644 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,7 @@ group :development, :test do gem "rack-test", "~> 0.6.2" gem "rspec", "~> 2.14", ">= 2.14.1" gem "rspec-html-matchers", "~> 0.4.3" + gem "rubocop", "~> 0.35.1", require: false gem "shotgun", "~> 0.9.0" gem "timecop", "~> 0.7.1" end diff --git a/Gemfile.lock b/Gemfile.lock index c73d2b1b2..c1f0ed65b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,6 +17,9 @@ GEM thread_safe (~> 0.1) tzinfo (~> 0.3.37) arel (4.0.2) + ast (2.1.0) + astrolabe (1.3.1) + parser (~> 2.2) backports (3.6.1) bcrypt-ruby (3.1.2) builder (3.1.4) @@ -67,7 +70,10 @@ GEM multi_json (1.11.0) nokogiri (1.6.1) mini_portile (~> 0.5.0) + parser (2.2.3.0) + ast (>= 1.1, < 3.0) pg (0.17.1) + powerpack (0.1.1) pry (0.9.12.4) coderay (~> 1.0) method_source (~> 0.8) @@ -85,6 +91,7 @@ GEM racksh (1.0.0) rack (>= 1.0) rack-test (>= 0.5) + rainbow (2.0.0) raindrops (0.13.0) rake (10.1.1) rest-client (1.6.7) @@ -100,6 +107,14 @@ GEM nokogiri (>= 1.4.4) rspec (>= 2.0.0) rspec-mocks (2.14.4) + rubocop (0.35.1) + astrolabe (~> 1.3) + parser (>= 2.2.3.0, < 3.0) + powerpack (~> 0.1) + rainbow (>= 1.99.1, < 3.0) + ruby-progressbar (~> 1.7) + tins (<= 1.6.0) + ruby-progressbar (1.7.5) sax-machine (0.2.1) nokogiri (~> 1.6.0) shotgun (0.9) @@ -175,6 +190,7 @@ DEPENDENCIES rake (~> 10.1, >= 10.1.1) rspec (~> 2.14, >= 2.14.1) rspec-html-matchers (~> 0.4.3) + rubocop (~> 0.35.1) shotgun (~> 0.9.0) sinatra (~> 1.4, >= 1.4.4) sinatra-activerecord (~> 1.2, >= 1.2.3) From 15052a457e9233c8bd1b5add3ba1d28cc413ccc1 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Tue, 8 Dec 2015 06:36:20 +0100 Subject: [PATCH 0099/1107] Adds a rubocop stylecheck to the travis setup --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index db74816c0..ced5d0559 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,4 +18,5 @@ rvm: script: - bundle exec rspec - mocha-phantomjs http://localhost:4567/test + - bundle exec rubocop sudo: false From 03686da6c41e031f9620100bcfc3ea937f0e50a9 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Mon, 7 Dec 2015 21:19:58 +0100 Subject: [PATCH 0100/1107] Fixes various spacing related rubocop offences --- .rubocop_todo.yml | 59 +++---------------- Rakefile | 4 +- app.rb | 4 +- app/commands/feeds/add_new_feed.rb | 2 +- app/commands/feeds/export_to_opml.rb | 2 +- app/commands/stories/mark_all_as_read.rb | 1 - app/commands/stories/mark_as_read.rb | 1 - app/commands/stories/mark_as_starred.rb | 1 - app/commands/stories/mark_as_unread.rb | 2 - app/commands/stories/mark_as_unstarred.rb | 2 - app/commands/stories/mark_feed_as_read.rb | 1 - app/commands/stories/mark_group_as_read.rb | 1 - app/commands/users/complete_setup.rb | 2 +- app/commands/users/create_user.rb | 6 +- app/commands/users/sign_in_user.rb | 2 +- app/controllers/debug_controller.rb | 2 +- app/controllers/first_run_controller.rb | 2 +- app/controllers/stories_controller.rb | 4 +- app/helpers/authentication_helpers.rb | 2 +- app/jobs/fetch_feed_job.rb | 2 +- app/models/feed.rb | 2 +- app/models/migration_status.rb | 2 +- app/models/story.rb | 2 +- app/repositories/feed_repository.rb | 1 - app/repositories/story_repository.rb | 1 - app/repositories/user_repository.rb | 4 +- app/tasks/fetch_feed.rb | 8 +-- app/utils/api_key.rb | 2 +- app/utils/opml_parser.rb | 2 +- app/utils/sample_story.rb | 28 ++++----- db/migrate/20130409010826_create_stories.rb | 4 +- db/migrate/20130425222157_add_delayed_job.rb | 2 +- fever_api.rb | 1 - spec/commands/feeds/export_to_opml_spec.rb | 2 +- .../commands/stories/mark_all_as_read_spec.rb | 2 +- spec/commands/stories/mark_as_read_spec.rb | 1 - spec/commands/stories/mark_as_starred.rb | 1 - spec/commands/stories/mark_as_unread_spec.rb | 2 - .../stories/mark_as_unstarred_spec.rb | 2 - .../users/change_user_password_spec.rb | 8 +-- spec/commands/users/complete_setup_spec.rb | 2 +- spec/commands/users/create_user_spec.rb | 2 +- spec/commands/users/sign_in_user_spec.rb | 2 +- spec/controllers/first_run_controller_spec.rb | 4 +- spec/controllers/stories_controller_spec.rb | 2 +- spec/factories/story_factory.rb | 2 +- spec/factories/user_factory.rb | 2 +- spec/fever_api_spec.rb | 4 +- spec/tasks/fetch_feed_spec.rb | 2 +- spec/utils/feed_discovery_spec.rb | 8 +-- spec/utils/i18n_support_spec.rb | 1 - spec/utils/opml_parser_spec.rb | 2 +- 52 files changed, 75 insertions(+), 137 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index cd8b153c4..f5d978c6d 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2015-12-07 21:05:42 +0100 using RuboCop version 0.34.2. +# on 2015-12-07 21:19:03 +0100 using RuboCop version 0.34.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -64,7 +64,7 @@ Metrics/ClassLength: Metrics/CyclomaticComplexity: Max: 9 -# Offense count: 137 +# Offense count: 131 # Configuration parameters: AllowURI, URISchemes. Metrics/LineLength: Max: 352 @@ -193,13 +193,12 @@ Style/EmptyLinesAroundAccessModifier: - 'app/models/story.rb' - 'app/tasks/fetch_feed.rb' -# Offense count: 4 +# Offense count: 2 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. Style/EmptyLinesAroundBlockBody: Exclude: - 'app/utils/opml_parser.rb' - - 'db/schema.rb' - 'spec/models/story_spec.rb' # Offense count: 2 @@ -292,10 +291,10 @@ Style/Next: - 'app/commands/feeds/import_from_opml.rb' - 'app/repositories/story_repository.rb' -# Offense count: 11 +# Offense count: 10 # Cop supports --auto-correct. Style/NumericLiterals: - MinDigits: 15 + MinDigits: 11 # Offense count: 4 # Cop supports --auto-correct. @@ -358,36 +357,11 @@ Style/SingleLineMethods: Exclude: - 'app/utils/sample_story.rb' -# Offense count: 29 +# Offense count: 6 # Cop supports --auto-correct. Style/SingleSpaceBeforeFirstArg: Exclude: - - 'app.rb' - 'db/migrate/20130425222157_add_delayed_job.rb' - - 'db/schema.rb' - -# Offense count: 6 -# Cop supports --auto-correct. -Style/SpaceAfterComma: - Exclude: - - 'app/models/story.rb' - - 'app/utils/opml_parser.rb' - - 'spec/fever_api_spec.rb' - - 'spec/tasks/fetch_feed_spec.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -Style/SpaceAroundEqualsInParameterDefault: - Enabled: false - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: MultiSpaceAllowedForOperators. -Style/SpaceAroundOperators: - Exclude: - - 'app.rb' - - 'spec/factories/story_factory.rb' # Offense count: 32 # Cop supports --auto-correct. @@ -395,12 +369,6 @@ Style/SpaceAroundOperators: Style/SpaceBeforeBlockBraces: Enabled: false -# Offense count: 1 -# Cop supports --auto-correct. -Style/SpaceBeforeComma: - Exclude: - - 'app/models/feed.rb' - # Offense count: 18 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. @@ -421,7 +389,7 @@ Style/SpaceInsideParens: - 'spec/fever_api/read_feeds_spec.rb' - 'spec/fever_api/read_items_spec.rb' -# Offense count: 960 +# Offense count: 906 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. Style/StringLiterals: @@ -449,12 +417,6 @@ Style/SymbolProc: - 'app/fever_api/sync_saved_item_ids.rb' - 'app/fever_api/sync_unread_item_ids.rb' -# Offense count: 30 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -Style/TrailingBlankLines: - Enabled: false - # Offense count: 3 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyleForMultiline, SupportedStyles. @@ -462,12 +424,7 @@ Style/TrailingComma: Exclude: - 'spec/fever_api/read_items_spec.rb' -# Offense count: 42 -# Cop supports --auto-correct. -Style/TrailingWhitespace: - Enabled: false - -# Offense count: 7 +# Offense count: 5 # Cop supports --auto-correct. # Configuration parameters: WordRegex. Style/WordArray: diff --git a/Rakefile b/Rakefile index 0a0193574..491cd24cd 100644 --- a/Rakefile +++ b/Rakefile @@ -47,7 +47,7 @@ task :work_jobs do Delayed::Job.delete_all 3.times do - Delayed::Worker.new(:min_priority => ENV['MIN_PRIORITY'], + Delayed::Worker.new(:min_priority => ENV['MIN_PRIORITY'], :max_priority => ENV['MAX_PRIORITY']).start end end @@ -81,4 +81,4 @@ begin task :default => [:speedy_tests] rescue LoadError # allow for bundle install --without development:test -end \ No newline at end of file +end diff --git a/app.rb b/app.rb index d26a7c95d..1526df86f 100644 --- a/app.rb +++ b/app.rb @@ -13,7 +13,7 @@ require_relative "app/repositories/user_repository" I18n.load_path += Dir[File.join(File.dirname(__FILE__), 'config/locales', '*.yml').to_s] -I18n.config.enforce_available_locales=false +I18n.config.enforce_available_locales = false class Stringer < Sinatra::Base # need to exclude assets for sinatra assetpack, see https://github.com/swanson/stringer/issues/112 @@ -81,7 +81,7 @@ def t(*args) "/css/styles.css" ] - js_compression :jsmin + js_compression :jsmin css_compression :simple prebuild true unless ENV['RACK_ENV'] == 'test' diff --git a/app/commands/feeds/add_new_feed.rb b/app/commands/feeds/add_new_feed.rb index f6e9a2ba3..1334e1aed 100644 --- a/app/commands/feeds/add_new_feed.rb +++ b/app/commands/feeds/add_new_feed.rb @@ -12,4 +12,4 @@ def self.add(url, discoverer = FeedDiscovery.new, repo = Feed) url: result.feed_url, last_fetched: Time.now - ONE_DAY) end -end \ No newline at end of file +end diff --git a/app/commands/feeds/export_to_opml.rb b/app/commands/feeds/export_to_opml.rb index 8822b057d..27d2d4ebd 100644 --- a/app/commands/feeds/export_to_opml.rb +++ b/app/commands/feeds/export_to_opml.rb @@ -26,4 +26,4 @@ def to_xml builder.to_xml end -end \ No newline at end of file +end diff --git a/app/commands/stories/mark_all_as_read.rb b/app/commands/stories/mark_all_as_read.rb index 4e0500472..e458c1f02 100644 --- a/app/commands/stories/mark_all_as_read.rb +++ b/app/commands/stories/mark_all_as_read.rb @@ -10,4 +10,3 @@ def mark_as_read @repo.fetch_by_ids(@story_ids).update_all(is_read: true) end end - diff --git a/app/commands/stories/mark_as_read.rb b/app/commands/stories/mark_as_read.rb index ea42df246..93fb679ae 100644 --- a/app/commands/stories/mark_as_read.rb +++ b/app/commands/stories/mark_as_read.rb @@ -10,4 +10,3 @@ def mark_as_read @repo.fetch(@story_id).update_attributes(is_read: true) end end - diff --git a/app/commands/stories/mark_as_starred.rb b/app/commands/stories/mark_as_starred.rb index 1449372e1..aff11e67f 100644 --- a/app/commands/stories/mark_as_starred.rb +++ b/app/commands/stories/mark_as_starred.rb @@ -10,4 +10,3 @@ def mark_as_starred @repo.fetch(@story_id).update_attributes(is_starred: true) end end - diff --git a/app/commands/stories/mark_as_unread.rb b/app/commands/stories/mark_as_unread.rb index 3eb26cc24..d1d90fb3c 100644 --- a/app/commands/stories/mark_as_unread.rb +++ b/app/commands/stories/mark_as_unread.rb @@ -10,5 +10,3 @@ def mark_as_unread @repo.fetch(@story_id).update_attributes(is_read: false) end end - - diff --git a/app/commands/stories/mark_as_unstarred.rb b/app/commands/stories/mark_as_unstarred.rb index 797f864f3..f6e6afe9c 100644 --- a/app/commands/stories/mark_as_unstarred.rb +++ b/app/commands/stories/mark_as_unstarred.rb @@ -10,5 +10,3 @@ def mark_as_unstarred @repo.fetch(@story_id).update_attributes(is_starred: false) end end - - diff --git a/app/commands/stories/mark_feed_as_read.rb b/app/commands/stories/mark_feed_as_read.rb index bf664fb7a..13894162e 100644 --- a/app/commands/stories/mark_feed_as_read.rb +++ b/app/commands/stories/mark_feed_as_read.rb @@ -11,4 +11,3 @@ def mark_feed_as_read @repo.fetch_unread_for_feed_by_timestamp(@feed_id, @timestamp).update_all(is_read: true) end end - diff --git a/app/commands/stories/mark_group_as_read.rb b/app/commands/stories/mark_group_as_read.rb index 9269e2c6b..286b0821c 100644 --- a/app/commands/stories/mark_group_as_read.rb +++ b/app/commands/stories/mark_group_as_read.rb @@ -20,4 +20,3 @@ def mark_group_as_read end end end - diff --git a/app/commands/users/complete_setup.rb b/app/commands/users/complete_setup.rb index f0518a11e..e37cb80bf 100644 --- a/app/commands/users/complete_setup.rb +++ b/app/commands/users/complete_setup.rb @@ -4,4 +4,4 @@ def self.complete(user) user.save user end -end \ No newline at end of file +end diff --git a/app/commands/users/create_user.rb b/app/commands/users/create_user.rb index 456eddd91..d3de6fe27 100644 --- a/app/commands/users/create_user.rb +++ b/app/commands/users/create_user.rb @@ -7,9 +7,9 @@ def initialize(repository = User) def create(password) @repo.delete_all - @repo.create(password: password, - password_confirmation: password, + @repo.create(password: password, + password_confirmation: password, setup_complete: false, api_key: ApiKey.compute(password)) end -end \ No newline at end of file +end diff --git a/app/commands/users/sign_in_user.rb b/app/commands/users/sign_in_user.rb index 71536bb2a..34891eeb1 100644 --- a/app/commands/users/sign_in_user.rb +++ b/app/commands/users/sign_in_user.rb @@ -11,4 +11,4 @@ def self.sign_in(submitted_password, repository = User) nil end end -end \ No newline at end of file +end diff --git a/app/controllers/debug_controller.rb b/app/controllers/debug_controller.rb index 5bb739a07..20276b70f 100644 --- a/app/controllers/debug_controller.rb +++ b/app/controllers/debug_controller.rb @@ -2,7 +2,7 @@ class Stringer < Sinatra::Base get "/debug" do - erb :"debug", locals: { + erb :"debug", locals: { queued_jobs_count: Delayed::Job.count, pending_migrations: MigrationStatus.new.pending_migrations } diff --git a/app/controllers/first_run_controller.rb b/app/controllers/first_run_controller.rb index c4bfbed67..9981de62a 100644 --- a/app/controllers/first_run_controller.rb +++ b/app/controllers/first_run_controller.rb @@ -46,4 +46,4 @@ def no_password(params) def password_mismatch?(params) params[:password] != params[:password_confirmation] end -end \ No newline at end of file +end diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index 1b18964ab..fb2d2452a 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -31,7 +31,7 @@ class Stringer < Sinatra::Base put "/stories/:id" do json_params = JSON.parse(request.body.read, symbolize_names: true) - + story = StoryRepository.fetch(params[:id]) story.is_read = !!json_params[:is_read] story.keep_unread = !!json_params[:keep_unread] @@ -42,7 +42,7 @@ class Stringer < Sinatra::Base post "/stories/mark_all_as_read" do MarkAllAsRead.new(params[:story_ids]).mark_as_read - + redirect to("/news") end end diff --git a/app/helpers/authentication_helpers.rb b/app/helpers/authentication_helpers.rb index ca5661159..e4af9a3d6 100644 --- a/app/helpers/authentication_helpers.rb +++ b/app/helpers/authentication_helpers.rb @@ -21,4 +21,4 @@ def current_user UserRepository.fetch(session[:user_id]) end end -end \ No newline at end of file +end diff --git a/app/jobs/fetch_feed_job.rb b/app/jobs/fetch_feed_job.rb index 36ce602c2..a7e362269 100644 --- a/app/jobs/fetch_feed_job.rb +++ b/app/jobs/fetch_feed_job.rb @@ -3,4 +3,4 @@ def perform feed = FeedRepository.fetch(feed_id) FetchFeed.new(feed).fetch end -end \ No newline at end of file +end diff --git a/app/models/feed.rb b/app/models/feed.rb index 006dcb9d9..6dea58551 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -1,5 +1,5 @@ class Feed < ActiveRecord::Base - has_many :stories, -> {order "published desc"} , dependent: :delete_all + has_many :stories, -> {order "published desc"}, dependent: :delete_all belongs_to :group validates_uniqueness_of :url diff --git a/app/models/migration_status.rb b/app/models/migration_status.rb index a96d98d09..4ed7262de 100644 --- a/app/models/migration_status.rb +++ b/app/models/migration_status.rb @@ -1,7 +1,7 @@ class MigrationStatus attr_reader :migrator - def initialize(migrator=ActiveRecord::Migrator) + def initialize(migrator = ActiveRecord::Migrator) @migrator = migrator end diff --git a/app/models/story.rb b/app/models/story.rb index d82f7b2e1..1d2b1f07f 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -12,7 +12,7 @@ def headline end def lead - strip_html(self.body)[0,100] + strip_html(self.body)[0, 100] end def source diff --git a/app/repositories/feed_repository.rb b/app/repositories/feed_repository.rb index 37fa9a822..3815b89fa 100644 --- a/app/repositories/feed_repository.rb +++ b/app/repositories/feed_repository.rb @@ -48,4 +48,3 @@ def self.valid_timestamp?(new_timestamp, current_timestamp) (current_timestamp.nil? || new_timestamp > current_timestamp) end end - diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index a46ca38b8..a898d2e44 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -141,4 +141,3 @@ def self.samples ] end end - diff --git a/app/repositories/user_repository.rb b/app/repositories/user_repository.rb index a30cada2a..029f26b85 100644 --- a/app/repositories/user_repository.rb +++ b/app/repositories/user_repository.rb @@ -3,7 +3,7 @@ class UserRepository def self.fetch(id) return nil unless id - + User.find(id) end @@ -19,4 +19,4 @@ def self.save(user) def self.first User.first end -end \ No newline at end of file +end diff --git a/app/tasks/fetch_feed.rb b/app/tasks/fetch_feed.rb index c1c9d2810..ba531bd05 100644 --- a/app/tasks/fetch_feed.rb +++ b/app/tasks/fetch_feed.rb @@ -17,13 +17,13 @@ def initialize(feed, parser: Feedjira::Feed, logger: nil) def fetch begin options = { - user_agent: USER_AGENT, - if_modified_since: @feed.last_fetched, - timeout: 30, + user_agent: USER_AGENT, + if_modified_since: @feed.last_fetched, + timeout: 30, max_redirects: 2, compress: true } - + raw_feed = @parser.fetch_and_parse(@feed.url, options) if raw_feed == 304 diff --git a/app/utils/api_key.rb b/app/utils/api_key.rb index 288494d90..6f0f52c4c 100644 --- a/app/utils/api_key.rb +++ b/app/utils/api_key.rb @@ -4,4 +4,4 @@ class ApiKey def self.compute(plaintext_password) Digest::MD5.hexdigest("stringer:#{plaintext_password}") end -end \ No newline at end of file +end diff --git a/app/utils/opml_parser.rb b/app/utils/opml_parser.rb index 118f342f5..c92a7d37f 100644 --- a/app/utils/opml_parser.rb +++ b/app/utils/opml_parser.rb @@ -4,7 +4,7 @@ class OpmlParser def parse_feeds(contents) doc = Nokogiri.XML(contents) - feeds_with_groups = Hash.new { |h,k| h[k] = [] } + feeds_with_groups = Hash.new { |h, k| h[k] = [] } doc.xpath('//body/outline').each do |outline| diff --git a/app/utils/sample_story.rb b/app/utils/sample_story.rb index dafbe0468..2ca1d9af6 100644 --- a/app/utils/sample_story.rb +++ b/app/utils/sample_story.rb @@ -4,22 +4,22 @@ def headline; title; end def permalink; "#"; end def lead; "Tofu shoreditch intelligentsia umami, fashion axe photo booth try-hard"; end def body - <<-eos -

    Tofu shoreditch intelligentsia umami, fashion axe photo booth try-hard terry - richardson quinoa actually fingerstache meggings fixie. Aesthetic salvia vinyl raw - denim, keffiyeh master cleanse tonx selfies mlkshk occupy twee street art gentrify. - Quinoa PBR readymade 90's. Chambray Austin aesthetic meggings, carles vinyl intelligentsia - tattooed. Keffiyeh mumblecore fingerstache, sartorial sriracha disrupt biodiesel cred. - Skateboard yr cosby sweater, narwhal beard ethnic jean shorts aesthetic. Post-ironic + <<-eos +

    Tofu shoreditch intelligentsia umami, fashion axe photo booth try-hard terry + richardson quinoa actually fingerstache meggings fixie. Aesthetic salvia vinyl raw + denim, keffiyeh master cleanse tonx selfies mlkshk occupy twee street art gentrify. + Quinoa PBR readymade 90's. Chambray Austin aesthetic meggings, carles vinyl intelligentsia + tattooed. Keffiyeh mumblecore fingerstache, sartorial sriracha disrupt biodiesel cred. + Skateboard yr cosby sweater, narwhal beard ethnic jean shorts aesthetic. Post-ironic flannel mlkshk, pickled VHS wolf banjo forage portland wayfarers.

    -

    Selfies mumblecore odd future irony DIY messenger bag. Authentic neutra next - level selvage squid. Four loko freegan occupy, tousled vinyl leggings selvage messenger - bag. Four loko wayfarers kale chips, next level banksy banh mi umami flannel hella. - Street art odd future scenester, intelligentsia brunch fingerstache YOLO narwhal - single-origin coffee tousled tumblr pop-up four loko you probably haven't heard of them - dreamcatcher. Single-origin coffee direct trade retro biodiesel, truffaut fanny pack - portland blue bottle scenester bushwick. Skateboard squid fanny pack bushwick, photo +

    Selfies mumblecore odd future irony DIY messenger bag. Authentic neutra next + level selvage squid. Four loko freegan occupy, tousled vinyl leggings selvage messenger + bag. Four loko wayfarers kale chips, next level banksy banh mi umami flannel hella. + Street art odd future scenester, intelligentsia brunch fingerstache YOLO narwhal + single-origin coffee tousled tumblr pop-up four loko you probably haven't heard of them + dreamcatcher. Single-origin coffee direct trade retro biodiesel, truffaut fanny pack + portland blue bottle scenester bushwick. Skateboard squid fanny pack bushwick, photo booth vice literally.

    eos end diff --git a/db/migrate/20130409010826_create_stories.rb b/db/migrate/20130409010826_create_stories.rb index 459a8f046..0d9bcc91e 100644 --- a/db/migrate/20130409010826_create_stories.rb +++ b/db/migrate/20130409010826_create_stories.rb @@ -4,9 +4,9 @@ def change t.string :title t.string :permalink t.text :body - + t.references :feed - + t.timestamps end end diff --git a/db/migrate/20130425222157_add_delayed_job.rb b/db/migrate/20130425222157_add_delayed_job.rb index 99589902b..88c0c197f 100644 --- a/db/migrate/20130425222157_add_delayed_job.rb +++ b/db/migrate/20130425222157_add_delayed_job.rb @@ -19,4 +19,4 @@ def self.up def self.down drop_table :delayed_jobs end -end \ No newline at end of file +end diff --git a/fever_api.rb b/fever_api.rb index d15e7477c..a84d336a5 100644 --- a/fever_api.rb +++ b/fever_api.rb @@ -36,4 +36,3 @@ def build_response(params) FeverAPI::Response.new(params).to_json end end - diff --git a/spec/commands/feeds/export_to_opml_spec.rb b/spec/commands/feeds/export_to_opml_spec.rb index bf6db1cba..0ca886ce9 100644 --- a/spec/commands/feeds/export_to_opml_spec.rb +++ b/spec/commands/feeds/export_to_opml_spec.rb @@ -33,4 +33,4 @@ title.content.should eq "Feeds from Stringer" end end -end \ No newline at end of file +end diff --git a/spec/commands/stories/mark_all_as_read_spec.rb b/spec/commands/stories/mark_all_as_read_spec.rb index 890d4584e..fc79573ab 100644 --- a/spec/commands/stories/mark_all_as_read_spec.rb +++ b/spec/commands/stories/mark_all_as_read_spec.rb @@ -6,7 +6,7 @@ describe "#mark_as_read" do let(:stories) { double } let(:repo){ double(fetch_by_ids: stories) } - + it "marks all stories as read" do command = MarkAllAsRead.new([1, 2], repo) stories.should_receive(:update_all).with(is_read: true) diff --git a/spec/commands/stories/mark_as_read_spec.rb b/spec/commands/stories/mark_as_read_spec.rb index 84c39a776..839ffa530 100644 --- a/spec/commands/stories/mark_as_read_spec.rb +++ b/spec/commands/stories/mark_as_read_spec.rb @@ -14,4 +14,3 @@ end end end - diff --git a/spec/commands/stories/mark_as_starred.rb b/spec/commands/stories/mark_as_starred.rb index 4a33939a8..c48c13683 100644 --- a/spec/commands/stories/mark_as_starred.rb +++ b/spec/commands/stories/mark_as_starred.rb @@ -14,4 +14,3 @@ end end end - diff --git a/spec/commands/stories/mark_as_unread_spec.rb b/spec/commands/stories/mark_as_unread_spec.rb index 7cc4f0283..a1757b761 100644 --- a/spec/commands/stories/mark_as_unread_spec.rb +++ b/spec/commands/stories/mark_as_unread_spec.rb @@ -14,5 +14,3 @@ end end end - - diff --git a/spec/commands/stories/mark_as_unstarred_spec.rb b/spec/commands/stories/mark_as_unstarred_spec.rb index 215f62840..ee7ca9601 100644 --- a/spec/commands/stories/mark_as_unstarred_spec.rb +++ b/spec/commands/stories/mark_as_unstarred_spec.rb @@ -14,5 +14,3 @@ end end end - - diff --git a/spec/commands/users/change_user_password_spec.rb b/spec/commands/users/change_user_password_spec.rb index 222bceb1c..36ff42bd9 100644 --- a/spec/commands/users/change_user_password_spec.rb +++ b/spec/commands/users/change_user_password_spec.rb @@ -14,20 +14,20 @@ it "changes the password of the user" do repo.should_receive(:first).and_return(user) repo.should_receive(:save) - + command = ChangeUserPassword.new(repo) result = command.change_user_password(new_password) - + BCrypt::Password.new(result.password_digest).should eq new_password end it "changes the API key of the user" do repo.should_receive(:first).and_return(user) repo.should_receive(:save) - + command = ChangeUserPassword.new(repo) result = command.change_user_password(new_password) - + result.api_key.should eq ApiKey.compute(new_password) end end diff --git a/spec/commands/users/complete_setup_spec.rb b/spec/commands/users/complete_setup_spec.rb index 7b289caf4..2d972ebb2 100644 --- a/spec/commands/users/complete_setup_spec.rb +++ b/spec/commands/users/complete_setup_spec.rb @@ -10,4 +10,4 @@ result = CompleteSetup.complete(user) result.setup_complete.should be_true end -end \ No newline at end of file +end diff --git a/spec/commands/users/create_user_spec.rb b/spec/commands/users/create_user_spec.rb index 24ac93b29..2a1627347 100644 --- a/spec/commands/users/create_user_spec.rb +++ b/spec/commands/users/create_user_spec.rb @@ -11,7 +11,7 @@ repo.should_receive(:create) repo.should_receive(:delete_all) - + command.create("password") end end diff --git a/spec/commands/users/sign_in_user_spec.rb b/spec/commands/users/sign_in_user_spec.rb index eb8d02797..4d218e80d 100644 --- a/spec/commands/users/sign_in_user_spec.rb +++ b/spec/commands/users/sign_in_user_spec.rb @@ -5,7 +5,7 @@ describe SignInUser do let(:valid_password) { "valid-pw" } let(:repo) { double(first: user) } - + let(:user) do double(password_digest: BCrypt::Password.create(valid_password), id: 1) end diff --git a/spec/controllers/first_run_controller_spec.rb b/spec/controllers/first_run_controller_spec.rb index ec2b9bb78..fdd185413 100644 --- a/spec/controllers/first_run_controller_spec.rb +++ b/spec/controllers/first_run_controller_spec.rb @@ -49,7 +49,7 @@ let(:user) { double } let(:feeds) {[double, double]} - before do + before do UserRepository.stub(fetch: user) Feed.stub(all: feeds) end @@ -57,7 +57,7 @@ it "displays the tutorial and completes setup" do CompleteSetup.should_receive(:complete).with(user).once FetchFeeds.should_receive(:enqueue).with(feeds).once - + get "/setup/tutorial" page = last_response.body diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index eddf3f932..dd130ea9c 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -13,7 +13,7 @@ StoryRepository.stub(:unread).and_return(stories) UserRepository.stub(fetch: double) end - + it "display list of unread stories" do get "/news" diff --git a/spec/factories/story_factory.rb b/spec/factories/story_factory.rb index f9d2d8598..f31e93b08 100644 --- a/spec/factories/story_factory.rb +++ b/spec/factories/story_factory.rb @@ -34,6 +34,6 @@ def self.build(params = {}) feed: params[:feed] || FeedFactory.build, is_read: params[:is_read] || false, is_starred: params[:is_starred] || false, - published: params[:published] ||Time.now) + published: params[:published] || Time.now) end end diff --git a/spec/factories/user_factory.rb b/spec/factories/user_factory.rb index 804d71477..da91ad011 100644 --- a/spec/factories/user_factory.rb +++ b/spec/factories/user_factory.rb @@ -8,4 +8,4 @@ def self.build id: rand(100), setup_complete: false) end -end \ No newline at end of file +end diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index fc4dc38af..719f1e1e2 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -158,7 +158,7 @@ def make_request(extra_headers = {}) last_response.should be_ok last_response_as_object.should include(standard_answer) last_response_as_object.should include( - unread_item_ids: [story_one.id,story_two.id].join(',') + unread_item_ids: [story_one.id, story_two.id].join(',') ) end @@ -170,7 +170,7 @@ def make_request(extra_headers = {}) last_response.should be_ok last_response_as_object.should include(standard_answer) last_response_as_object.should include( - saved_item_ids: [story_one.id,story_two.id].join(',') + saved_item_ids: [story_one.id, story_two.id].join(',') ) end end diff --git a/spec/tasks/fetch_feed_spec.rb b/spec/tasks/fetch_feed_spec.rb index ba061719a..6f631844b 100644 --- a/spec/tasks/fetch_feed_spec.rb +++ b/spec/tasks/fetch_feed_spec.rb @@ -6,7 +6,7 @@ let(:daring_fireball) do double(id: 1, url: "http://daringfireball.com/feed", - last_fetched: Time.new(2013,1,1), + last_fetched: Time.new(2013, 1, 1), stories: []) end diff --git a/spec/utils/feed_discovery_spec.rb b/spec/utils/feed_discovery_spec.rb index fbfb2f19c..05439e9ec 100644 --- a/spec/utils/feed_discovery_spec.rb +++ b/spec/utils/feed_discovery_spec.rb @@ -17,7 +17,7 @@ finder.should_receive(:find).and_return([]) result = FeedDiscovery.new.discover(url, finder, parser) - + result.should be_false end @@ -25,7 +25,7 @@ parser.should_receive(:fetch_and_parse).with(url, anything).and_return(feed) result = FeedDiscovery.new.discover(url, finder, parser) - + result.should eq feed end @@ -35,7 +35,7 @@ parser.should_receive(:fetch_and_parse).with(invalid_discovered_url, anything).and_raise(StandardError) result = FeedDiscovery.new.discover(url, finder, parser) - + result.should be_false end @@ -45,7 +45,7 @@ parser.should_receive(:fetch_and_parse).with(valid_discovered_url, anything).and_return(feed) result = FeedDiscovery.new.discover(url, finder, parser) - + result.should eq feed end end diff --git a/spec/utils/i18n_support_spec.rb b/spec/utils/i18n_support_spec.rb index ed8f053e9..2645b5bc4 100644 --- a/spec/utils/i18n_support_spec.rb +++ b/spec/utils/i18n_support_spec.rb @@ -33,4 +33,3 @@ end end end - diff --git a/spec/utils/opml_parser_spec.rb b/spec/utils/opml_parser_spec.rb index 28ed073be..193d351c7 100644 --- a/spec/utils/opml_parser_spec.rb +++ b/spec/utils/opml_parser_spec.rb @@ -4,7 +4,7 @@ describe OpmlParser do let(:parser) { OpmlParser.new } - + describe "#parse_feeds" do it "it returns a hash of feed details from an OPML file" do result = parser.parse_feeds(<<-eos) From 8efdbe5f58b27a88dc7b50bc7096d2e5558467f6 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Tue, 8 Dec 2015 15:42:11 +0100 Subject: [PATCH 0101/1107] Don't allow failures for ruby 2.1 and 2.2 builds --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index ced5d0559..293e22e4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,6 @@ before_script: - sleep 5 cache: bundler language: ruby -matrix: - allow_failures: - - rvm: 2.1.0 - - rvm: 2.2.0 rvm: - 2.0.0 - 2.1.0 From 1fa68f54a82d07aa64f76137a192a9eab0ab810e Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Tue, 8 Dec 2015 15:43:17 +0100 Subject: [PATCH 0102/1107] Use the latest 2.1 and 2.2 ruby versions for the test builds --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 293e22e4a..97b589969 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,8 +9,8 @@ cache: bundler language: ruby rvm: - 2.0.0 - - 2.1.0 - - 2.2.0 + - 2.1 + - 2.2 script: - bundle exec rspec - mocha-phantomjs http://localhost:4567/test From 93bac0a4ff30df2c18e39ca57603dd4cda9fd21b Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Tue, 8 Dec 2015 20:57:28 +0100 Subject: [PATCH 0103/1107] Fix Lint/HandleExceptions Both of these occurrences appear perfectly legitimate to me. --- .rubocop_todo.yml | 6 ------ Rakefile | 2 +- app/repositories/story_repository.rb | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f5d978c6d..38e30e6ee 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,12 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 2 -Lint/HandleExceptions: - Exclude: - - 'Rakefile' - - 'app/repositories/story_repository.rb' - # Offense count: 2 Lint/RescueException: Exclude: diff --git a/Rakefile b/Rakefile index 491cd24cd..fca20c804 100644 --- a/Rakefile +++ b/Rakefile @@ -79,6 +79,6 @@ begin RSpec::Core::RakeTask.new(:spec) task :default => [:speedy_tests] -rescue LoadError +rescue LoadError # rubocop:disable Lint/HandleExceptions # allow for bundle install --without development:test end diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index a898d2e44..d42f38706 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -109,7 +109,7 @@ def self.expand_absolute_urls(content, base_url) unless url =~ abs_re begin node.set_attribute(attr, URI.join(base_url, url).to_s) - rescue URI::InvalidURIError + rescue URI::InvalidURIError # rubocop:disable Lint/HandleExceptions # Just ignore. If we cannot parse the url, we don't want the entire # import to blow up. end From 8bbc329d925c2f9b89b03e11532e2db70b188936 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Tue, 8 Dec 2015 21:03:31 +0100 Subject: [PATCH 0104/1107] Fix Lint/ShadowingOuterLocalVariable --- .rubocop_todo.yml | 5 ----- spec/support/active_record.rb | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 38e30e6ee..f9f3c5117 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -12,11 +12,6 @@ Lint/RescueException: - 'app/tasks/fetch_feed.rb' - 'app/utils/feed_discovery.rb' -# Offense count: 1 -Lint/ShadowingOuterLocalVariable: - Exclude: - - 'spec/support/active_record.rb' - # Offense count: 6 # Cop supports --auto-correct. Lint/UnusedBlockArgument: diff --git a/spec/support/active_record.rb b/spec/support/active_record.rb index 8054b3e7b..fd13a8512 100644 --- a/spec/support/active_record.rb +++ b/spec/support/active_record.rb @@ -1,7 +1,7 @@ require 'active_record' -config = YAML.load(File.read('config/database.yml')) -ActiveRecord::Base.establish_connection(config['test']) +db_config = YAML.load(File.read('config/database.yml')) +ActiveRecord::Base.establish_connection(db_config['test']) ActiveRecord::Base.logger = Logger.new('log/test.log') def need_to_migrate? From bfb8518defb7f4539688acf9a2f6b59a49d59fd4 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Tue, 8 Dec 2015 21:05:04 +0100 Subject: [PATCH 0105/1107] Fix Lint/UnusedBlockArgument --- .rubocop_todo.yml | 7 ------- Rakefile | 4 ++-- config/unicorn.rb | 4 ++-- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f9f3c5117..0e595d357 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -12,13 +12,6 @@ Lint/RescueException: - 'app/tasks/fetch_feed.rb' - 'app/utils/feed_discovery.rb' -# Offense count: 6 -# Cop supports --auto-correct. -Lint/UnusedBlockArgument: - Exclude: - - 'Rakefile' - - 'config/unicorn.rb' - # Offense count: 5 # Cop supports --auto-correct. Lint/UnusedMethodArgument: diff --git a/Rakefile b/Rakefile index fca20c804..c4f67f84d 100644 --- a/Rakefile +++ b/Rakefile @@ -33,7 +33,7 @@ task :lazy_fetch do end desc "Fetch single feed" -task :fetch_feed, :id do |t, args| +task :fetch_feed, :id do |_t, args| FetchFeed.new(Feed.find(args[:id])).fetch end @@ -58,7 +58,7 @@ task :change_password do end desc "Clean up old stories that are read and unstarred" -task :cleanup_old_stories, :number_of_days do |t, args| +task :cleanup_old_stories, :number_of_days do |_t, args| args.with_defaults(:number_of_days => 30) RemoveOldStories.remove!(args[:number_of_days].to_i) end diff --git a/config/unicorn.rb b/config/unicorn.rb index ed15845cf..cb8293edb 100644 --- a/config/unicorn.rb +++ b/config/unicorn.rb @@ -4,7 +4,7 @@ @delayed_job_pid = nil -before_fork do |server, worker| +before_fork do |_server, _worker| # the following is highly recommended for Rails + "preload_app true" # as there's no need for the master process to hold a connection defined?(ActiveRecord::Base) and @@ -15,7 +15,7 @@ sleep 1 end -after_fork do |server, worker| +after_fork do |_server, _worker| if defined?(ActiveRecord::Base) env = ENV['RACK_ENV'] || "development" config = YAML::load(ERB.new(File.read('config/database.yml')).result)[env] From c190d3c0bff7a297363a4dea4f5ca34adc4b8172 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Tue, 8 Dec 2015 21:06:19 +0100 Subject: [PATCH 0106/1107] Fix Lint/UnusedMethodArgument Mostly by prefixing the unused arguments with `_` to indicate they're unused. This is because the unused arguments are often parts of protocols/interfaces. Does remove the unused `parser` parameter from `get_feed_for_url`, though. --- .rubocop_todo.yml | 10 ---------- app/fever_api/authentication.rb | 2 +- app/models/story.rb | 2 +- app/utils/feed_discovery.rb | 6 +++--- app/utils/sample_story.rb | 2 +- spec/support/feed_server.rb | 2 +- 6 files changed, 7 insertions(+), 17 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 0e595d357..319ed8886 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -12,16 +12,6 @@ Lint/RescueException: - 'app/tasks/fetch_feed.rb' - 'app/utils/feed_discovery.rb' -# Offense count: 5 -# Cop supports --auto-correct. -Lint/UnusedMethodArgument: - Exclude: - - 'app/fever_api/authentication.rb' - - 'app/models/story.rb' - - 'app/utils/feed_discovery.rb' - - 'app/utils/sample_story.rb' - - 'spec/support/feed_server.rb' - # Offense count: 1 Lint/UselessAccessModifier: Exclude: diff --git a/app/fever_api/authentication.rb b/app/fever_api/authentication.rb index 00088cb6c..520640a71 100644 --- a/app/fever_api/authentication.rb +++ b/app/fever_api/authentication.rb @@ -4,7 +4,7 @@ def initialize(options = {}) @clock = options.fetch(:clock){ Time } end - def call(params) + def call(_params) { auth: 1, last_refreshed_on_time: @clock.now.to_i } end end diff --git a/app/models/story.rb b/app/models/story.rb index 1d2b1f07f..71259f3bc 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -23,7 +23,7 @@ def pretty_date I18n.l(self.published) end - def as_json(options = {}) + def as_json(_options = {}) super(methods: [:headline, :lead, :source, :pretty_date]) end diff --git a/app/utils/feed_discovery.rb b/app/utils/feed_discovery.rb index 3b448c911..970da870b 100644 --- a/app/utils/feed_discovery.rb +++ b/app/utils/feed_discovery.rb @@ -3,17 +3,17 @@ class FeedDiscovery def discover(url, finder = Feedbag, parser = Feedjira::Feed) - get_feed_for_url(url, finder, parser) do + get_feed_for_url(url, parser) do urls = finder.find(url) return false if urls.empty? - get_feed_for_url(urls.first, finder, parser) do + get_feed_for_url(urls.first, parser) do return false end end end - def get_feed_for_url(url, finder, parser) + def get_feed_for_url(url, parser) feed = parser.fetch_and_parse(url, user_agent: "Stringer") feed.feed_url ||= url feed diff --git a/app/utils/sample_story.rb b/app/utils/sample_story.rb index 2ca1d9af6..e7adaf240 100644 --- a/app/utils/sample_story.rb +++ b/app/utils/sample_story.rb @@ -28,7 +28,7 @@ def keep_unread; false; end def is_starred; false; end def published; Time.now; end - def as_json(options = {}) + def as_json(_options = {}) { id: id, headline: headline, diff --git a/spec/support/feed_server.rb b/spec/support/feed_server.rb index e6abdbc5a..502c9b338 100644 --- a/spec/support/feed_server.rb +++ b/spec/support/feed_server.rb @@ -5,7 +5,7 @@ def initialize @server = Capybara::Server.new(method(:response)).boot end - def response(env) + def response(_env) [200, {}, [@response]] end From 531166f694f2e829fa43d5468e48d00e9a9a50b2 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Tue, 8 Dec 2015 21:08:02 +0100 Subject: [PATCH 0107/1107] Fix Lint/UselessAccessModifier --- .rubocop_todo.yml | 5 ----- app/repositories/feed_repository.rb | 2 -- 2 files changed, 7 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 319ed8886..16b47f56e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -12,11 +12,6 @@ Lint/RescueException: - 'app/tasks/fetch_feed.rb' - 'app/utils/feed_discovery.rb' -# Offense count: 1 -Lint/UselessAccessModifier: - Exclude: - - 'app/repositories/feed_repository.rb' - # Offense count: 5 Lint/UselessAssignment: Exclude: diff --git a/app/repositories/feed_repository.rb b/app/repositories/feed_repository.rb index 3815b89fa..beae4c158 100644 --- a/app/repositories/feed_repository.rb +++ b/app/repositories/feed_repository.rb @@ -41,8 +41,6 @@ def self.in_group Feed.where('group_id IS NOT NULL') end - private - def self.valid_timestamp?(new_timestamp, current_timestamp) new_timestamp && new_timestamp.year >= MIN_YEAR && (current_timestamp.nil? || new_timestamp > current_timestamp) From f00301fc3e5d1b12ebf01af92e79201b36ae1ce7 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Tue, 8 Dec 2015 21:09:18 +0100 Subject: [PATCH 0108/1107] Fix Lint/UselessAssignment --- .rubocop_todo.yml | 6 ------ app/tasks/change_password.rb | 2 +- spec/repositories/feed_repository_spec.rb | 8 ++++---- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 16b47f56e..62126f6c4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -12,12 +12,6 @@ Lint/RescueException: - 'app/tasks/fetch_feed.rb' - 'app/utils/feed_discovery.rb' -# Offense count: 5 -Lint/UselessAssignment: - Exclude: - - 'app/tasks/change_password.rb' - - 'spec/repositories/feed_repository_spec.rb' - # Offense count: 5 Metrics/AbcSize: Max: 37 diff --git a/app/tasks/change_password.rb b/app/tasks/change_password.rb index f67b09805..8a3f67011 100644 --- a/app/tasks/change_password.rb +++ b/app/tasks/change_password.rb @@ -8,7 +8,7 @@ def initialize(command = ChangeUserPassword.new) end def change_password - while (password = ask_password) != (confirmation = ask_confirmation) + while (password = ask_password) != ask_confirmation puts "The confirmation doesn't match the password. Please try again." end @command.change_user_password(password) diff --git a/spec/repositories/feed_repository_spec.rb b/spec/repositories/feed_repository_spec.rb index 9111dbd9b..ca86372bc 100644 --- a/spec/repositories/feed_repository_spec.rb +++ b/spec/repositories/feed_repository_spec.rb @@ -10,7 +10,7 @@ it "saves the last_fetched timestamp" do feed = Feed.new - result = FeedRepository.update_last_fetched(feed, timestamp) + FeedRepository.update_last_fetched(feed, timestamp) feed.last_fetched.should eq timestamp end @@ -20,7 +20,7 @@ it "rejects weird timestamps" do feed = Feed.new(last_fetched: timestamp) - result = FeedRepository.update_last_fetched(feed, weird_timestamp) + FeedRepository.update_last_fetched(feed, weird_timestamp) feed.last_fetched.should eq timestamp end @@ -28,7 +28,7 @@ it "doesn't update if timestamp is nil (feed does not report last modified)" do feed = Feed.new(last_fetched: timestamp) - result = FeedRepository.update_last_fetched(feed, nil) + FeedRepository.update_last_fetched(feed, nil) feed.last_fetched.should eq timestamp end @@ -37,7 +37,7 @@ feed = Feed.new(last_fetched: timestamp) one_week_ago = timestamp - 1.week - result = FeedRepository.update_last_fetched(feed, one_week_ago) + FeedRepository.update_last_fetched(feed, one_week_ago) feed.last_fetched.should eq timestamp end From 0ae414410d7072bcbae6524c81efff7d31279fe8 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Mon, 14 Dec 2015 22:02:24 +0100 Subject: [PATCH 0109/1107] Fix Lint/RescueException This is done by rescuing `StandardError` instead of `Exception`, which should still cover most if not all errors raised by the Curb and Feedjira libraries. We no longer try to rescue fatal errors, like `NoMemoryError`, which is what RuboCop was complaining about. An even better fix for this could be to rescue an even narrower set of errors, but I'll leave that until the next time. --- .rubocop_todo.yml | 6 ------ app/tasks/fetch_feed.rb | 3 +-- app/utils/feed_discovery.rb | 2 +- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 62126f6c4..ce6a59997 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,12 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 2 -Lint/RescueException: - Exclude: - - 'app/tasks/fetch_feed.rb' - - 'app/utils/feed_discovery.rb' - # Offense count: 5 Metrics/AbcSize: Max: 37 diff --git a/app/tasks/fetch_feed.rb b/app/tasks/fetch_feed.rb index ba531bd05..015d9da9d 100644 --- a/app/tasks/fetch_feed.rb +++ b/app/tasks/fetch_feed.rb @@ -5,7 +5,6 @@ require_relative "../commands/feeds/find_new_stories" class FetchFeed - USER_AGENT = "Stringer (https://github.com/swanson/stringer)" def initialize(feed, parser: Feedjira::Feed, logger: nil) @@ -37,7 +36,7 @@ def fetch end FeedRepository.set_status(:green, @feed) - rescue Exception => ex + rescue => ex FeedRepository.set_status(:red, @feed) @logger.error "Something went wrong when parsing #{@feed.url}: #{ex}" if @logger diff --git a/app/utils/feed_discovery.rb b/app/utils/feed_discovery.rb index 970da870b..656865bfd 100644 --- a/app/utils/feed_discovery.rb +++ b/app/utils/feed_discovery.rb @@ -17,7 +17,7 @@ def get_feed_for_url(url, parser) feed = parser.fetch_and_parse(url, user_agent: "Stringer") feed.feed_url ||= url feed - rescue Exception + rescue yield if block_given? end end From 22e1cb34b236864f56cc350098b589de1a8026e4 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Fri, 18 Dec 2015 22:14:17 +0100 Subject: [PATCH 0110/1107] Uses html5 style for autofocus attributes --- app/views/feeds/add.erb | 2 +- app/views/feeds/edit.erb | 4 ++-- app/views/first_run/password.erb | 2 +- app/views/sessions/new.erb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/feeds/add.erb b/app/views/feeds/add.erb index 510151915..7ead0fde7 100644 --- a/app/views/feeds/add.erb +++ b/app/views/feeds/add.erb @@ -9,7 +9,7 @@
    - +
    diff --git a/app/views/feeds/edit.erb b/app/views/feeds/edit.erb index 7209fe014..dae5278e1 100644 --- a/app/views/feeds/edit.erb +++ b/app/views/feeds/edit.erb @@ -9,12 +9,12 @@
    - +
    - +
    diff --git a/app/views/first_run/password.erb b/app/views/first_run/password.erb index 0c22fce03..90a3d6394 100644 --- a/app/views/first_run/password.erb +++ b/app/views/first_run/password.erb @@ -7,7 +7,7 @@
    - +
    diff --git a/app/views/sessions/new.erb b/app/views/sessions/new.erb index 5c55438a7..2a5bc8183 100644 --- a/app/views/sessions/new.erb +++ b/app/views/sessions/new.erb @@ -7,7 +7,7 @@
    - +
    From f31924980aa8a8ec1499b5c014f3c72b6ca8fc67 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Fri, 18 Dec 2015 22:14:31 +0100 Subject: [PATCH 0111/1107] Only marks the Feed#url field with autofocus Only one autofocus element per page is allowed. --- app/views/feeds/edit.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/feeds/edit.erb b/app/views/feeds/edit.erb index dae5278e1..6ce35a6df 100644 --- a/app/views/feeds/edit.erb +++ b/app/views/feeds/edit.erb @@ -9,7 +9,7 @@
    - +
    From 3090422a1dbe5a36d90f5d710231e5957562692c Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Sat, 19 Dec 2015 14:06:19 +0100 Subject: [PATCH 0112/1107] Fix Performance/StringReplacement > ### delete([other_str]+) -> new_str > > Returns a copy of *str* with all characters in the intersection of its > arguments deleted. Uses the same rules for building the set of characters as > `String#count`. See . --- .rubocop_todo.yml | 7 ------- db/migrate/20140421224454_fix_invalid_unicode.rb | 2 +- ...2103617_fix_invalid_titles_with_unicode_line_endings.rb | 2 +- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index ce6a59997..47d7d6b87 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -33,13 +33,6 @@ Metrics/MethodLength: Metrics/PerceivedComplexity: Max: 9 -# Offense count: 4 -# Cop supports --auto-correct. -Performance/StringReplacement: - Exclude: - - 'db/migrate/20140421224454_fix_invalid_unicode.rb' - - 'db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb' - # Offense count: 14 # Cop supports --auto-correct. # Configuration parameters: EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle, SupportedLastArgumentHashStyles. diff --git a/db/migrate/20140421224454_fix_invalid_unicode.rb b/db/migrate/20140421224454_fix_invalid_unicode.rb index f32fde2eb..d9200cd6c 100644 --- a/db/migrate/20140421224454_fix_invalid_unicode.rb +++ b/db/migrate/20140421224454_fix_invalid_unicode.rb @@ -1,7 +1,7 @@ class FixInvalidUnicode < ActiveRecord::Migration def up Story.find_each do |story| - valid_body = story.body.gsub("\u2028", '').gsub("\u2029", '') + valid_body = story.body.delete("\u2028").delete("\u2029") story.update_attribute(:body, valid_body) end end diff --git a/db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb b/db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb index e10762619..4202b0bc8 100644 --- a/db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb +++ b/db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb @@ -2,7 +2,7 @@ class FixInvalidTitlesWithUnicodeLineEndings < ActiveRecord::Migration def up Story.find_each do |story| unless story.title.nil? - valid_title = story.title.gsub("\u2028", '').gsub("\u2029", '') + valid_title = story.title.delete("\u2028").delete("\u2029") story.update_attribute(:title, valid_title) end end From a854041b6afb87facdbdbd32b96b4e42c5dda523 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Fri, 18 Dec 2015 21:50:23 +0100 Subject: [PATCH 0113/1107] Adds group_id to Feeds#edit --- app/controllers/feeds_controller.rb | 2 +- app/public/css/styles.css | 10 +++++++++- app/repositories/feed_repository.rb | 3 ++- app/views/feeds/edit.erb | 12 ++++++++++++ config/locales/en.yml | 1 + spec/controllers/feeds_controller_spec.rb | 12 +++++++++++- 6 files changed, 36 insertions(+), 4 deletions(-) diff --git a/app/controllers/feeds_controller.rb b/app/controllers/feeds_controller.rb index 460ca3756..375609d4b 100644 --- a/app/controllers/feeds_controller.rb +++ b/app/controllers/feeds_controller.rb @@ -18,7 +18,7 @@ class Stringer < Sinatra::Base put "/feeds/:id" do feed = FeedRepository.fetch(params[:id]) - FeedRepository.update_feed(feed, params[:feed_name], params[:feed_url]) + FeedRepository.update_feed(feed, params[:feed_name], params[:feed_url], params[:group_id]) flash[:success] = t('feeds.edit.flash.updated_successfully') redirect to('/feeds') diff --git a/app/public/css/styles.css b/app/public/css/styles.css index 79535fcc7..f60a61406 100644 --- a/app/public/css/styles.css +++ b/app/public/css/styles.css @@ -489,11 +489,19 @@ li.feed .remove-feed a:hover { transition: 0.25s; } -.setup #password, .setup #password-confirmation, .setup #feed-url, .setup #feed-name { +.setup #password, .setup #password-confirmation, .setup #feed-url, .setup #feed-name, .setup input.select-dummy { padding-left: 100px; padding-right: 36px; } +.setup select { + display: block; + position: absolute; + left: 100px; + top: 5px; + width: 244px; +} + .setup .control-group { position: relative; } diff --git a/app/repositories/feed_repository.rb b/app/repositories/feed_repository.rb index beae4c158..919d89962 100644 --- a/app/repositories/feed_repository.rb +++ b/app/repositories/feed_repository.rb @@ -11,9 +11,10 @@ def self.fetch_by_ids(ids) Feed.where(id: ids) end - def self.update_feed(feed, name, url) + def self.update_feed(feed, name, url, group_id = nil) feed.name = name feed.url = url + feed.group_id = group_id feed.save end diff --git a/app/views/feeds/edit.erb b/app/views/feeds/edit.erb index 6ce35a6df..7f0565daf 100644 --- a/app/views/feeds/edit.erb +++ b/app/views/feeds/edit.erb @@ -18,6 +18,18 @@
    + <% if Group.any? %> +
    + + + +
    + <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index ee60d8bfe..8eb38af8d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -34,6 +34,7 @@ en: fields: feed_name: Feed Name feed_url: Feed URL + group: Group submit: Save flash: updated_successfully: Updated the feed for ya'! diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index 92f204b5b..e4fffaf8e 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -42,12 +42,22 @@ it "updates a feed given the id" do feed = FeedFactory.build(url: 'example.com/atom') FeedRepository.should_receive(:fetch).with("123").and_return(feed) - FeedRepository.should_receive(:update_feed).with(feed, 'Test', 'example.com/feed') + FeedRepository.should_receive(:update_feed).with(feed, 'Test', 'example.com/feed', nil) put "/feeds/123", feed_id: "123", feed_name: "Test", feed_url: "example.com/feed" last_response.should be_redirect end + + it "updates a feed group given the id" do + feed = FeedFactory.build(url: 'example.com/atom') + FeedRepository.should_receive(:fetch).with("123").and_return(feed) + FeedRepository.should_receive(:update_feed).with(feed, feed.name, feed.url, "321") + + put "/feeds/123", feed_id: "123", feed_name: feed.name, feed_url: feed.url, group_id: "321" + + last_response.should be_redirect + end end describe "DELETE /feeds/:feed_id" do From 6b2fbd56a42adc48f69f176bc29cd8dd2792f44a Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Tue, 8 Dec 2015 22:05:35 +0100 Subject: [PATCH 0114/1107] Fix Metrics/AbcSize ...through some light refactorings. --- .rubocop_todo.yml | 4 ---- app/fever_api/response.rb | 35 ++++++++++++++++++-------------- app/fever_api/write_mark_item.rb | 28 ++++++++++++++----------- app/tasks/fetch_feed.rb | 25 +++++++++++++++-------- app/utils/opml_parser.rb | 26 +++++++++++++++++------- spec/factories/story_factory.rb | 18 ++++++++-------- 6 files changed, 81 insertions(+), 55 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 47d7d6b87..6f31e614b 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,10 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 5 -Metrics/AbcSize: - Max: 37 - # Offense count: 1 # Configuration parameters: CountComments. Metrics/ClassLength: diff --git a/app/fever_api/response.rb b/app/fever_api/response.rb index 4656eabad..2b24247f6 100644 --- a/app/fever_api/response.rb +++ b/app/fever_api/response.rb @@ -16,28 +16,33 @@ module FeverAPI class Response - def initialize(params) - @response = { api_version: 3 } + ACTIONS = [ + Authentication, - @response.merge! Authentication.new.call(params) + ReadFeeds, + ReadGroups, + ReadFeedsGroups, + ReadFavicons, + ReadItems, + ReadLinks, - @response.merge! ReadFeeds.new.call(params) - @response.merge! ReadGroups.new.call(params) - @response.merge! ReadFeedsGroups.new.call(params) - @response.merge! ReadFavicons.new.call(params) - @response.merge! ReadItems.new.call(params) - @response.merge! ReadLinks.new.call(params) + SyncUnreadItemIds, + SyncSavedItemIds, - @response.merge! SyncUnreadItemIds.new.call(params) - @response.merge! SyncSavedItemIds.new.call(params) + WriteMarkItem, + WriteMarkFeed, + WriteMarkGroup + ] - @response.merge! WriteMarkItem.new.call(params) - @response.merge! WriteMarkFeed.new.call(params) - @response.merge! WriteMarkGroup.new.call(params) + def initialize(params) + @params = params end def to_json - @response.to_json + base_response = { api_version: 3 } + ACTIONS + .inject(base_response) { |a, e| a.merge!(e.new.call(@params)) } + .to_json end end end diff --git a/app/fever_api/write_mark_item.rb b/app/fever_api/write_mark_item.rb index b3f1fd15f..a2752e013 100644 --- a/app/fever_api/write_mark_item.rb +++ b/app/fever_api/write_mark_item.rb @@ -13,20 +13,24 @@ def initialize(options = {}) end def call(params = {}) - if params[:mark] == "item" - case params[:as] - when "read" - @read_marker_class.new(params[:id]).mark_as_read - when "unread" - @unread_marker_class.new(params[:id]).mark_as_unread - when "saved" - @starred_marker_class.new(params[:id]).mark_as_starred - when "unsaved" - @unstarred_marker_class.new(params[:id]).mark_as_unstarred - end - end + mark_item_as(params[:id], params[:as]) if params[:mark] == "item" {} end + + private + + def mark_item_as(id, as) + case as + when "read" + @read_marker_class.new(id).mark_as_read + when "unread" + @unread_marker_class.new(id).mark_as_unread + when "saved" + @starred_marker_class.new(id).mark_as_starred + when "unsaved" + @unstarred_marker_class.new(id).mark_as_unstarred + end + end end end diff --git a/app/tasks/fetch_feed.rb b/app/tasks/fetch_feed.rb index 015d9da9d..c1b23ecd3 100644 --- a/app/tasks/fetch_feed.rb +++ b/app/tasks/fetch_feed.rb @@ -15,15 +15,7 @@ def initialize(feed, parser: Feedjira::Feed, logger: nil) def fetch begin - options = { - user_agent: USER_AGENT, - if_modified_since: @feed.last_fetched, - timeout: 30, - max_redirects: 2, - compress: true - } - - raw_feed = @parser.fetch_and_parse(@feed.url, options) + raw_feed = fetch_raw_feed if raw_feed == 304 @logger.info "#{@feed.url} has not been modified since last fetch" if @logger @@ -44,11 +36,26 @@ def fetch end private + + def fetch_raw_feed + @parser.fetch_and_parse(@feed.url, options) + end + def new_entries_from(raw_feed) finder = FindNewStories.new(raw_feed, @feed.id, @feed.last_fetched, latest_entry_id) finder.new_stories end + def options + { + user_agent: USER_AGENT, + if_modified_since: @feed.last_fetched, + timeout: 30, + max_redirects: 2, + compress: true + } + end + def latest_entry_id return @feed.stories.first.entry_id unless @feed.stories.empty? end diff --git a/app/utils/opml_parser.rb b/app/utils/opml_parser.rb index c92a7d37f..f88f7dff8 100644 --- a/app/utils/opml_parser.rb +++ b/app/utils/opml_parser.rb @@ -2,13 +2,10 @@ class OpmlParser def parse_feeds(contents) - doc = Nokogiri.XML(contents) - feeds_with_groups = Hash.new { |h, k| h[k] = [] } - doc.xpath('//body/outline').each do |outline| - - if outline.attributes['xmlUrl'].nil? # it's a group! + outlines_in(contents).each do |outline| + if outline_is_group?(outline) group_name = extract_name(outline.attributes).value feeds = outline.xpath('./outline') else # it's a top-level feed, which means it's a feed without group @@ -17,16 +14,31 @@ def parse_feeds(contents) end feeds.each do |feed| - feeds_with_groups[group_name] << { name: extract_name(feed.attributes).value, - url: feed.attributes['xmlUrl'].value } + feeds_with_groups[group_name] << feed_to_hash(feed) end end + feeds_with_groups end private + def outlines_in(contents) + Nokogiri.XML(contents).xpath('//body/outline') + end + + def outline_is_group?(outline) + outline.attributes['xmlUrl'].nil? + end + def extract_name(attributes) attributes['title'] || attributes['text'] end + + def feed_to_hash(feed) + { + name: extract_name(feed.attributes).value, + url: feed.attributes['xmlUrl'].value + } + end end diff --git a/spec/factories/story_factory.rb b/spec/factories/story_factory.rb index f31e93b08..405318dfc 100644 --- a/spec/factories/story_factory.rb +++ b/spec/factories/story_factory.rb @@ -26,14 +26,16 @@ def as_fever_json end def self.build(params = {}) - FakeStory.new( + default_params = { id: rand(100), - title: params[:title] || Faker::Lorem.sentence, - permalink: params[:permalink] || Faker::Internet.url, - body: params[:body] || Faker::Lorem.paragraph, - feed: params[:feed] || FeedFactory.build, - is_read: params[:is_read] || false, - is_starred: params[:is_starred] || false, - published: params[:published] || Time.now) + title: Faker::Lorem.sentence, + permalink: Faker::Internet.url, + body: Faker::Lorem.paragraph, + feed: FeedFactory.build, + is_read: false, + is_starred: false, + published: Time.now + } + FakeStory.new(default_params.merge(params)) end end From 34f09dae3f6ae6edb58c9e273ef6619a6a256e07 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Mon, 14 Dec 2015 22:49:48 +0100 Subject: [PATCH 0115/1107] Fix Metrics/CyclomaticComplexity ...through a small refactoring of `Sinatra::AuthenticationHelpers#needs_authentication?`. --- .rubocop_todo.yml | 4 -- app/helpers/authentication_helpers.rb | 5 +- spec/helpers/authentications_helper_spec.rb | 58 +++++++++++++++++++++ 3 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 spec/helpers/authentications_helper_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6f31e614b..5b589f061 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -11,10 +11,6 @@ Metrics/ClassLength: Max: 108 -# Offense count: 2 -Metrics/CyclomaticComplexity: - Max: 9 - # Offense count: 131 # Configuration parameters: AllowURI, URISchemes. Metrics/LineLength: diff --git a/app/helpers/authentication_helpers.rb b/app/helpers/authentication_helpers.rb index e4af9a3d6..75b43ee46 100644 --- a/app/helpers/authentication_helpers.rb +++ b/app/helpers/authentication_helpers.rb @@ -11,9 +11,8 @@ def is_authenticated? def needs_authentication?(path) return false if ENV['RACK_ENV'] == 'test' return false if !UserRepository.setup_complete? - return false if path == "/login" || path == "/logout" - return false if path =~ /css/ || path =~ /js/ || path =~ /img/ - return false if path == "/heroku" + return false if %w(/login /logout /heroku).include?(path) + return false if path =~ /css|js|img/ true end diff --git a/spec/helpers/authentications_helper_spec.rb b/spec/helpers/authentications_helper_spec.rb new file mode 100644 index 000000000..325dce20b --- /dev/null +++ b/spec/helpers/authentications_helper_spec.rb @@ -0,0 +1,58 @@ +require "spec_helper" + +app_require "helpers/authentication_helpers" + +RSpec.describe Sinatra::AuthenticationHelpers do + class Helper + include Sinatra::AuthenticationHelpers + end + + let(:helper) { Helper.new } + + describe "#needs_authentication?" do + let(:authenticated_path) { "/news" } + + before do + stub_const("ENV", "RACK_ENV" => "not-test") + allow(UserRepository).to receive(:setup_complete?).and_return(true) + end + + context "when `RACK_ENV` is 'test'" do + it "returns false" do + stub_const("ENV", "RACK_ENV" => "test") + + expect(helper.needs_authentication?(authenticated_path)).to eq(false) + end + end + + context "when setup in not complete" do + it "returns false" do + allow(UserRepository).to receive(:setup_complete?).and_return(false) + + expect(helper.needs_authentication?(authenticated_path)).to eq(false) + end + end + + %w(/login /logout /heroku).each do |path| + context "when `path` is '#{path}'" do + it "returns false" do + expect(helper.needs_authentication?(path)).to eq(false) + end + end + end + + %w(css js img).each do |path| + context "when `path` contains '#{path}'" do + it "returns false" do + expect(helper.needs_authentication?("/#{path}/file.ext")).to eq(false) + end + end + end + + context "otherwise" do + it "returns true" do + expect(helper.needs_authentication?(authenticated_path)).to eq(true) + end + end + end +end From 8531c835342bf38783bc21beca02a7bd495c2c9c Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Thu, 31 Dec 2015 15:40:42 +0100 Subject: [PATCH 0116/1107] Set Metrics/LineLength maximum to 120 characters Matt and I agreed that 120 characters is a sensible maximum line length for modern editors, see . Also set the `MaxLineLength` option for two other cops, since they're related. Joins lines in the Fever API files to fix warnings from `Style/IfUnlessModifier`. --- .rubocop.yml | 9 +++++ .rubocop_todo.yml | 5 --- app/commands/feeds/find_new_stories.rb | 6 +-- app/fever_api/write_mark_feed.rb | 4 +- app/fever_api/write_mark_group.rb | 4 +- db/migrate/20130425222157_add_delayed_job.rb | 42 +++++++++++++++----- spec/repositories/story_repository_spec.rb | 10 ++++- 7 files changed, 52 insertions(+), 28 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index cc19aafc0..b394a5efd 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,3 +4,12 @@ AllCops: Exclude: - 'db/schema.rb' - 'vendor/**/*' + +Metrics/LineLength: + Max: 120 + +Style/IfUnlessModifier: + MaxLineLength: 120 + +Style/WhileUntilModifier: + MaxLineLength: 120 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 5b589f061..17631b04c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -11,11 +11,6 @@ Metrics/ClassLength: Max: 108 -# Offense count: 131 -# Configuration parameters: AllowURI, URISchemes. -Metrics/LineLength: - Max: 352 - # Offense count: 11 # Configuration parameters: CountComments. Metrics/MethodLength: diff --git a/app/commands/feeds/find_new_stories.rb b/app/commands/feeds/find_new_stories.rb index 823e48fa5..e8bd10678 100644 --- a/app/commands/feeds/find_new_stories.rb +++ b/app/commands/feeds/find_new_stories.rb @@ -15,10 +15,8 @@ def new_stories @raw_feed.entries.each do |story| break if @latest_entry_id && story.id == @latest_entry_id - - unless story_age_exceeds_threshold?(story) || StoryRepository.exists?(story.id, @feed_id) - stories << story - end + next if story_age_exceeds_threshold?(story) || StoryRepository.exists?(story.id, @feed_id) + stories << story end stories diff --git a/app/fever_api/write_mark_feed.rb b/app/fever_api/write_mark_feed.rb index b4b582e6b..1c4d72961 100644 --- a/app/fever_api/write_mark_feed.rb +++ b/app/fever_api/write_mark_feed.rb @@ -7,9 +7,7 @@ def initialize(options = {}) end def call(params = {}) - if params[:mark] == "feed" - @marker_class.new(params[:id], params[:before]).mark_feed_as_read - end + @marker_class.new(params[:id], params[:before]).mark_feed_as_read if params[:mark] == "feed" {} end diff --git a/app/fever_api/write_mark_group.rb b/app/fever_api/write_mark_group.rb index 7ccbe127b..3ef39a0e1 100644 --- a/app/fever_api/write_mark_group.rb +++ b/app/fever_api/write_mark_group.rb @@ -7,9 +7,7 @@ def initialize(options = {}) end def call(params = {}) - if params[:mark] == "group" - @marker_class.new(params[:id], params[:before]).mark_group_as_read - end + @marker_class.new(params[:id], params[:before]).mark_group_as_read if params[:mark] == "group" {} end diff --git a/db/migrate/20130425222157_add_delayed_job.rb b/db/migrate/20130425222157_add_delayed_job.rb index 88c0c197f..2043b904c 100644 --- a/db/migrate/20130425222157_add_delayed_job.rb +++ b/db/migrate/20130425222157_add_delayed_job.rb @@ -1,19 +1,39 @@ class AddDelayedJob < ActiveRecord::Migration def self.up - create_table :delayed_jobs, :force => true do |table| - table.integer :priority, :default => 0 # Allows some jobs to jump to the front of the queue - table.integer :attempts, :default => 0 # Provides for retries, but still fail eventually. - table.text :handler # YAML-encoded string of the object that will do work - table.text :last_error # reason for last failure (See Note below) - table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. - table.datetime :locked_at # Set when a client is working on this object - table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) - table.string :locked_by # Who is working on this object (if locked) - table.string :queue # The name of the queue this job is in + create_table :delayed_jobs, force: true do |table| + # Allows some jobs to jump to the front of the queue + table.integer :priority, default: 0 + + # Provides for retries, but still fail eventually. + table.integer :attempts, default: 0 + + # YAML-encoded string of the object that will do work + table.text :handler + + # reason for last failure (See Note below) + table.text :last_error + + # When to run. Could be Time.zone.now for immediately, or sometime in the + # future. + table.datetime :run_at + + # Set when a client is working on this object + table.datetime :locked_at + + # Set when all retries have failed (actually, by default, the record is + # deleted instead) + table.datetime :failed_at + + # Who is working on this object (if locked) + table.string :locked_by + + # The name of the queue this job is in + table.string :queue + table.timestamps end - add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority' + add_index :delayed_jobs, [:priority, :run_at], name: 'delayed_jobs_priority' end def self.down diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 35320dcb9..9efec9ff6 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -77,7 +77,11 @@ end it "leaves the url as-is if it cannot be parsed" do - weird_url = "https://github.com/aphyr/jepsen/blob/1403f2d6e61c595bafede0d404fd4a893371c036/elasticsearch/src/jepsen/system/elasticsearch.clj#L161-L226.%20Then%20we'll%20write%20a%20%5Bregister%20test%5D(https://github.com/aphyr/jepsen/blob/1403f2d6e61c595bafede0d404fd4a893371c036/elasticsearch/test/jepsen/system/elasticsearch_test.clj#L18-L50)" + weird_url = "https://github.com/aphyr/jepsen/blob/1403f2d6e61c595bafede0d404fd4a893371c036/" \ + "elasticsearch/src/jepsen/system/elasticsearch.clj" \ + "#L161-L226.%20Then%20we'll%20write%20a%20%5Bregister%20test%5D" \ + "(https://github.com/aphyr/jepsen/blob/1403f2d6e61c595bafede0d404fd4a893371c036/" \ + "elasticsearch/test/jepsen/system/elasticsearch_test.clj#L18-L50)" content = <<-EOS @@ -155,7 +159,9 @@ end it "resolves relative urls" do - url = StoryRepository.normalize_url("/progrium/dokku/releases/tag/v0.4.4", "https://github.com/progrium/dokku/releases.atom") + url = StoryRepository.normalize_url( + "/progrium/dokku/releases/tag/v0.4.4", "https://github.com/progrium/dokku/releases.atom" + ) url.should eq "https://github.com/progrium/dokku/releases/tag/v0.4.4" end end From 23e1f39d9e830197fb6e2b2c11f0cdab82b1acb8 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Sat, 19 Dec 2015 12:56:53 +0100 Subject: [PATCH 0117/1107] Lower Metrics/MethodLength maximum to 15 lines Despite my best efforts, I find it hard to keep all methods below or at the default maximum length of 10 lines. 15 lines is a somewhat arbitrary compromise that's subject to future tweaking. See also: --- .rubocop.yml | 3 ++ .rubocop_todo.yml | 5 ---- app/commands/feeds/export_to_opml.rb | 2 +- app/tasks/fetch_feed.rb | 20 +++++++++---- app/utils/sample_story.rb | 44 +++++++++++++++------------- 5 files changed, 42 insertions(+), 32 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index b394a5efd..b96f6e4ef 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,6 +8,9 @@ AllCops: Metrics/LineLength: Max: 120 +Metrics/MethodLength: + Max: 15 + Style/IfUnlessModifier: MaxLineLength: 120 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 17631b04c..d50318216 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -11,11 +11,6 @@ Metrics/ClassLength: Max: 108 -# Offense count: 11 -# Configuration parameters: CountComments. -Metrics/MethodLength: - Max: 22 - # Offense count: 2 Metrics/PerceivedComplexity: Max: 9 diff --git a/app/commands/feeds/export_to_opml.rb b/app/commands/feeds/export_to_opml.rb index 27d2d4ebd..7f69d25ce 100644 --- a/app/commands/feeds/export_to_opml.rb +++ b/app/commands/feeds/export_to_opml.rb @@ -5,7 +5,7 @@ def initialize(feeds) @feeds = feeds end - def to_xml + def to_xml # rubocop:disable Metrics/MethodLength builder = Nokogiri::XML::Builder.new do |xml| xml.opml(version: "1.0") do xml.head { diff --git a/app/tasks/fetch_feed.rb b/app/tasks/fetch_feed.rb index c1b23ecd3..37d00ab65 100644 --- a/app/tasks/fetch_feed.rb +++ b/app/tasks/fetch_feed.rb @@ -18,13 +18,9 @@ def fetch raw_feed = fetch_raw_feed if raw_feed == 304 - @logger.info "#{@feed.url} has not been modified since last fetch" if @logger + feed_not_modified else - new_entries_from(raw_feed).each do |entry| - StoryRepository.add(entry, @feed) - end - - FeedRepository.update_last_fetched(@feed, raw_feed.last_modified) + feed_modified(raw_feed) end FeedRepository.set_status(:green, @feed) @@ -41,6 +37,18 @@ def fetch_raw_feed @parser.fetch_and_parse(@feed.url, options) end + def feed_not_modified + @logger.info "#{@feed.url} has not been modified since last fetch" if @logger + end + + def feed_modified(raw_feed) + new_entries_from(raw_feed).each do |entry| + StoryRepository.add(entry, @feed) + end + + FeedRepository.update_last_fetched(@feed, raw_feed.last_modified) + end + def new_entries_from(raw_feed) finder = FindNewStories.new(raw_feed, @feed.id, @feed.last_fetched, latest_entry_id) finder.new_stories diff --git a/app/utils/sample_story.rb b/app/utils/sample_story.rb index e7adaf240..e8c29d15c 100644 --- a/app/utils/sample_story.rb +++ b/app/utils/sample_story.rb @@ -1,28 +1,32 @@ class SampleStory < Struct.new(:source, :title, :lead, :is_read, :published) + BODY = <<-eos +

    Tofu shoreditch intelligentsia umami, fashion axe photo booth +try-hard terry richardson quinoa actually fingerstache meggings fixie. Aesthetic +salvia vinyl raw denim, keffiyeh master cleanse tonx selfies mlkshk occupy twee +street art gentrify. Quinoa PBR readymade 90's. Chambray Austin aesthetic +meggings, carles vinyl intelligentsia tattooed. Keffiyeh mumblecore +fingerstache, sartorial sriracha disrupt biodiesel cred. Skateboard yr cosby +sweater, narwhal beard ethnic jean shorts aesthetic. Post-ironic flannel mlkshk, +pickled VHS wolf banjo forage portland wayfarers.

    + +

    Selfies mumblecore odd future irony DIY messenger bag. +Authentic neutra next level selvage squid. Four loko freegan occupy, tousled +vinyl leggings selvage messenger bag. Four loko wayfarers kale chips, next level +banksy banh mi umami flannel hella. Street art odd future scenester, +intelligentsia brunch fingerstache YOLO narwhal single-origin coffee tousled +tumblr pop-up four loko you probably haven't heard of them dreamcatcher. +Single-origin coffee direct trade retro biodiesel, truffaut fanny pack portland +blue bottle scenester bushwick. Skateboard squid fanny pack bushwick, photo +booth vice literally.

    + eos + def id; -1 * rand(100); end def headline; title; end def permalink; "#"; end - def lead; "Tofu shoreditch intelligentsia umami, fashion axe photo booth try-hard"; end - def body - <<-eos -

    Tofu shoreditch intelligentsia umami, fashion axe photo booth try-hard terry - richardson quinoa actually fingerstache meggings fixie. Aesthetic salvia vinyl raw - denim, keffiyeh master cleanse tonx selfies mlkshk occupy twee street art gentrify. - Quinoa PBR readymade 90's. Chambray Austin aesthetic meggings, carles vinyl intelligentsia - tattooed. Keffiyeh mumblecore fingerstache, sartorial sriracha disrupt biodiesel cred. - Skateboard yr cosby sweater, narwhal beard ethnic jean shorts aesthetic. Post-ironic - flannel mlkshk, pickled VHS wolf banjo forage portland wayfarers.

    - -

    Selfies mumblecore odd future irony DIY messenger bag. Authentic neutra next - level selvage squid. Four loko freegan occupy, tousled vinyl leggings selvage messenger - bag. Four loko wayfarers kale chips, next level banksy banh mi umami flannel hella. - Street art odd future scenester, intelligentsia brunch fingerstache YOLO narwhal - single-origin coffee tousled tumblr pop-up four loko you probably haven't heard of them - dreamcatcher. Single-origin coffee direct trade retro biodiesel, truffaut fanny pack - portland blue bottle scenester bushwick. Skateboard squid fanny pack bushwick, photo - booth vice literally.

    -eos + def lead + "Tofu shoreditch intelligentsia umami, fashion axe photo booth try-hard" end + def body; BODY; end def is_read; false; end def keep_unread; false; end def is_starred; false; end From d03c9476fc1853f94593973fc55b7a6b6f366e2d Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Sat, 19 Dec 2015 13:05:01 +0100 Subject: [PATCH 0118/1107] Fix Metrics/PerceivedComplexity --- .rubocop_todo.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d50318216..39da48ed0 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -11,10 +11,6 @@ Metrics/ClassLength: Max: 108 -# Offense count: 2 -Metrics/PerceivedComplexity: - Max: 9 - # Offense count: 14 # Cop supports --auto-correct. # Configuration parameters: EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle, SupportedLastArgumentHashStyles. From 6a3201fd170c7a9ec25d396eae144b6f77fb3005 Mon Sep 17 00:00:00 2001 From: Victor Koronen Date: Thu, 31 Dec 2015 16:03:11 +0100 Subject: [PATCH 0119/1107] Fix Metrics/ClassLength Fixes Metrics/ClassLength by extracting `StoryRepository#expand_absolute_urls` and `StoryRepository#normalize_url` to a separate helper module. --- .rubocop_todo.yml | 5 - app/helpers/url_helpers.rb | 39 +++++++ app/repositories/story_repository.rb | 37 +------ spec/helpers/url_helers_spec.rb | 112 +++++++++++++++++++++ spec/repositories/story_repository_spec.rb | 95 ----------------- 5 files changed, 154 insertions(+), 134 deletions(-) create mode 100644 app/helpers/url_helpers.rb create mode 100644 spec/helpers/url_helers_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 39da48ed0..38974983e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,11 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 1 -# Configuration parameters: CountComments. -Metrics/ClassLength: - Max: 108 - # Offense count: 14 # Cop supports --auto-correct. # Configuration parameters: EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle, SupportedLastArgumentHashStyles. diff --git a/app/helpers/url_helpers.rb b/app/helpers/url_helpers.rb new file mode 100644 index 000000000..3ff436f39 --- /dev/null +++ b/app/helpers/url_helpers.rb @@ -0,0 +1,39 @@ +require "nokogiri" +require "uri" + +module UrlHelpers + ABS_RE = URI::DEFAULT_PARSER.regexp[:ABS_URI] + + def expand_absolute_urls(content, base_url) + doc = Nokogiri::HTML.fragment(content) + + [["a", "href"], ["img", "src"], ["video", "src"]].each do |tag, attr| + doc.css("#{tag}[#{attr}]").each do |node| + url = node.get_attribute(attr) + next if url =~ ABS_RE + + begin + node.set_attribute(attr, URI.join(base_url, url).to_s) + rescue URI::InvalidURIError # rubocop:disable Lint/HandleExceptions + # Just ignore. If we cannot parse the url, we don't want the entire + # import to blow up. + end + end + end + + doc.to_html + end + + def normalize_url(url, base_url) + uri = URI.parse(url) + + # resolve (protocol) relative URIs + if uri.relative? + base_uri = URI.parse(base_url) + scheme = base_uri.scheme || 'http' + uri = URI.join("#{scheme}://#{base_uri.host}", uri) + end + + uri.to_s + end +end diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index d42f38706..0b65704f1 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -1,7 +1,10 @@ +require_relative "../helpers/url_helpers" require_relative "../models/story" require_relative "../utils/sample_story" class StoryRepository + extend UrlHelpers + def self.add(entry, feed) entry.url = normalize_url(entry.url, feed.url) @@ -99,40 +102,6 @@ def self.sanitize(content) .to_s end - def self.expand_absolute_urls(content, base_url) - doc = Nokogiri::HTML.fragment(content) - abs_re = URI::DEFAULT_PARSER.regexp[:ABS_URI] - - [["a", "href"], ["img", "src"], ["video", "src"]].each do |tag, attr| - doc.css("#{tag}[#{attr}]").each do |node| - url = node.get_attribute(attr) - unless url =~ abs_re - begin - node.set_attribute(attr, URI.join(base_url, url).to_s) - rescue URI::InvalidURIError # rubocop:disable Lint/HandleExceptions - # Just ignore. If we cannot parse the url, we don't want the entire - # import to blow up. - end - end - end - end - - doc.to_html - end - - def self.normalize_url(url, base_url) - uri = URI.parse(url) - base_uri = URI.parse(base_url) - - # resolve (protocol) relative URIs - if uri.relative? - scheme = base_uri.scheme || 'http' - uri = URI.join("#{scheme}://#{base_uri.host}", uri) - end - - uri.to_s - end - def self.samples [ SampleStory.new("Darin' Fireballs", "Why you should trade your firstborn for a Retina iPad"), diff --git a/spec/helpers/url_helers_spec.rb b/spec/helpers/url_helers_spec.rb new file mode 100644 index 000000000..c71d619b0 --- /dev/null +++ b/spec/helpers/url_helers_spec.rb @@ -0,0 +1,112 @@ +require "spec_helper" + +app_require "helpers/url_helpers" + +RSpec.describe UrlHelpers do + class Helper + include UrlHelpers + end + + let(:helper) { Helper.new } + + describe "#expand_absolute_urls" do + it "preserves existing absolute urls" do + content = 'bar' + + helper.expand_absolute_urls(content, nil).should eq content + end + + it "replaces relative urls in a, img and video tags" do + content = <<-EOS +
    + +tee + +
    + EOS + + result = helper.expand_absolute_urls(content, "http://oodl.io/d/") + result.gsub(/\n/, "").should eq <<-EOS.gsub(/\n/, "") +
    + +tee + + +
    + EOS + end + + it "handles empty body" do + helper.expand_absolute_urls("", nil).should eq "" + end + + it "doesn't modify tags that do not have url attributes" do + content = <<-EOS +
    + + + +
    + EOS + + result = helper.expand_absolute_urls(content, "http://oodl.io/d/") + result.gsub(/\n/, "").should eq <<-EOS.gsub(/\n/, "") +
    + + + +
    + EOS + end + + it "leaves the url as-is if it cannot be parsed" do + weird_url = "https://github.com/aphyr/jepsen/blob/" \ + "1403f2d6e61c595bafede0d404fd4a893371c036/" \ + "elasticsearch/src/jepsen/system/elasticsearch.clj#" \ + "L161-L226.%20Then%20we'll%20write%20a%20%5Bregister%20test%5D(" \ + "https://github.com/aphyr/jepsen/blob/" \ + "1403f2d6e61c595bafede0d404fd4a893371c036/" \ + "elasticsearch/test/jepsen/system/elasticsearch_test.clj#L18-L50)" + + content = "" + + result = helper.expand_absolute_urls(content, "http://oodl.io/d/") + result.should include(weird_url) + end + end + + describe "#normalize_url" do + it "resolves scheme-less urls" do + %w(http https).each do |scheme| + feed_url = "#{scheme}://blog.golang.org/feed.atom" + + url = helper.normalize_url("//blog.golang.org/context", feed_url) + + url.should eq "#{scheme}://blog.golang.org/context" + end + end + + it "leaves urls with a scheme intact" do + input = 'http://blog.golang.org/context' + normalized_url = helper.normalize_url( + input, 'http://blog.golang.org/feed.atom' + ) + normalized_url.should eq(input) + end + + it "falls back to http if the base_url is also sheme less" do + url = helper.normalize_url( + "//blog.golang.org/context", "//blog.golang.org/feed.atom" + ) + url.should eq 'http://blog.golang.org/context' + end + + it "resolves relative urls" do + url = helper.normalize_url( + "/progrium/dokku/releases/tag/v0.4.4", + "https://github.com/progrium/dokku/releases.atom" + ) + url.should eq "https://github.com/progrium/dokku/releases/tag/v0.4.4" + end + end +end diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 9efec9ff6..2bf169ebb 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -26,72 +26,6 @@ end end - describe ".expand_absolute_urls" do - it "preserves existing absolute urls" do - content = 'bar' - - StoryRepository.expand_absolute_urls(content, nil).should eq content - end - - it "replaces relative urls in a, img and video tags" do - content = <<-EOS -
    - -tee - -
    - EOS - - result = StoryRepository.expand_absolute_urls(content, "http://oodl.io/d/") - result.gsub(/\n/, "").should eq <<-EOS.gsub(/\n/, "") -
    - -tee - - -
    - EOS - end - - it "handles empty body" do - StoryRepository.expand_absolute_urls("", nil).should eq "" - end - - it "doesn't modify tags that do not have url attributes" do - content = <<-EOS -
    - - - -
    - EOS - - result = StoryRepository.expand_absolute_urls(content, "http://oodl.io/d/") - result.gsub(/\n/, "").should eq <<-EOS.gsub(/\n/, "") -
    - - - -
    - EOS - end - - it "leaves the url as-is if it cannot be parsed" do - weird_url = "https://github.com/aphyr/jepsen/blob/1403f2d6e61c595bafede0d404fd4a893371c036/" \ - "elasticsearch/src/jepsen/system/elasticsearch.clj" \ - "#L161-L226.%20Then%20we'll%20write%20a%20%5Bregister%20test%5D" \ - "(https://github.com/aphyr/jepsen/blob/1403f2d6e61c595bafede0d404fd4a893371c036/" \ - "elasticsearch/test/jepsen/system/elasticsearch_test.clj#L18-L50)" - - content = <<-EOS - - EOS - - result = StoryRepository.expand_absolute_urls(content, "http://oodl.io/d/") - result.should include(weird_url) - end - end - describe ".extract_content" do let(:entry) do double(url: "http://mdswanson.com", @@ -136,33 +70,4 @@ end end end - - describe ".normalize_url" do - it "resolves scheme-less urls" do - %w{http https}.each do |scheme| - feed_url = "#{scheme}://blog.golang.org/feed.atom" - - url = StoryRepository.normalize_url("//blog.golang.org/context", feed_url) - url.should eq "#{scheme}://blog.golang.org/context" - end - end - - it "leaves urls with a scheme intact" do - input = 'http://blog.golang.org/context' - normalized_url = StoryRepository.normalize_url(input, 'http://blog.golang.org/feed.atom') - normalized_url.should eq(input) - end - - it "falls back to http if the base_url is also sheme less" do - url = StoryRepository.normalize_url("//blog.golang.org/context", "//blog.golang.org/feed.atom") - url.should eq 'http://blog.golang.org/context' - end - - it "resolves relative urls" do - url = StoryRepository.normalize_url( - "/progrium/dokku/releases/tag/v0.4.4", "https://github.com/progrium/dokku/releases.atom" - ) - url.should eq "https://github.com/progrium/dokku/releases/tag/v0.4.4" - end - end end From 293632bbf797ebed618cd48314a785eb5317c248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Areias?= Date: Tue, 19 Jan 2016 14:00:37 +1100 Subject: [PATCH 0120/1107] Removing request to google fonts and adding fonts locally --- app/public/css/lato-fonts.css | 30 +++++++++++++++++++++++ app/public/fonts/lato/Lato-Black.woff2 | Bin 0 -> 176748 bytes app/public/fonts/lato/Lato-Bold.woff2 | Bin 0 -> 184912 bytes app/public/fonts/lato/Lato-Italic.woff2 | Bin 0 -> 195704 bytes app/public/fonts/lato/Lato-Light.woff2 | Bin 0 -> 181500 bytes app/public/fonts/lato/Lato-Regular.woff2 | Bin 0 -> 182708 bytes app/views/layout.erb | 2 +- 7 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 app/public/css/lato-fonts.css create mode 100644 app/public/fonts/lato/Lato-Black.woff2 create mode 100644 app/public/fonts/lato/Lato-Bold.woff2 create mode 100644 app/public/fonts/lato/Lato-Italic.woff2 create mode 100644 app/public/fonts/lato/Lato-Light.woff2 create mode 100644 app/public/fonts/lato/Lato-Regular.woff2 diff --git a/app/public/css/lato-fonts.css b/app/public/css/lato-fonts.css new file mode 100644 index 000000000..a74b8f3a8 --- /dev/null +++ b/app/public/css/lato-fonts.css @@ -0,0 +1,30 @@ +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 300; + src: local('Lato Light'), local('Lato-Light'), url('/fonts/lato/Lato-Light.woff2') format('woff2'); +} +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 400; + src: local('Lato Regular'), local('Lato-Regular'), url('/fonts/lato/Lato-Regular.woff2') format('woff2'); +} +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 700; + src: local('Lato Bold'), local('Lato-Bold'), url('/fonts/lato/Lato-Bold.woff2') format('woff2'); +} +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 900; + src: local('Lato Black'), local('Lato-Black'), url('/fonts/lato/Lato-Black.woff2') format('woff2'); +} +@font-face { + font-family: 'Lato'; + font-style: italic; + font-weight: 400; + src: local('Lato Italic'), local('Lato-Italic'), url('/fonts/lato/Lato-Italic.woff2') format('woff2'); +} diff --git a/app/public/fonts/lato/Lato-Black.woff2 b/app/public/fonts/lato/Lato-Black.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3ee7cd441e47372f956e43128696e300208eca9f GIT binary patch literal 176748 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 literal 0 HcmV?d00001 diff --git a/app/public/fonts/lato/Lato-Bold.woff2 b/app/public/fonts/lato/Lato-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..bb195043cfc07fa52741c6144d7378b5ba8be4c5 GIT binary patch literal 184912 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

    x{o9f|7gEmo2*wv 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 z zszDMPcOXcsJmc55^ndeO7g@Vzt3hi7h|_rvbdL8^vuFU8Q+Al0X-FsB>Z;N#pKLqs z@%UfjhIRxPbum)AiaGr@&QI4Li$pV=h7tied(NMcO)QqGx?SHe}w=kD_n%u+*SbOltvK*Nz+~|-#X%o$X@(@2e z;Tasa;7fOK*{45|IJ5p6SifpUvsiR=vQHoDd!oYKTF^>W?k!zB!Cpie-kJ*Aa*5r_%ZvAYe(olaOm9swIDmx?EE zZUijbQ>Kwr!laZBXmk|XAwOgV#){)YmIh*Pwj#E`k(J1&JQUg> zCwa9)pYz2Kj~d^l@TjQ~2@PUC=S2#|UXUC?%z{0pFnKh8jo;|P3T#b0V+hqIo24?T z++{%~$(fJ|wP?ipbGgN7_p?pHL!?uCS+43%L!}Q+pmQN)syTNQmsP6X!aMZ zq0lUAsF4BsQ26!;$!|mulT&Xr1NsBX)t;rG_R;-%ZuNpv zNIRcK8jRX;6~JPk6p)_cq3D(LVzXT^nkby*L~@%lt8hVKoHqNRAq;dck;bN|vOlcm z<6rx>`04TwxI;iMBjTuDRorI_SE$F#P0yNsL*Es$XZ0RP9!*_oV(uDn+lf*Bs_7AE zhd&{ekkOaXo(!+rqMtSrbfp=!m_=uM|uI|ZFp&UG;L>`z3JDr09_=XEY)!UqX zoT!zq1g#W-QJ+Jmm>NO(e=kw_xK8vjCC=cE(1As*eSQ_8yW*W-U#;$pp>x-2L;IfE zE`U&;mCjASTG^qyu%D2@Lc1nq5Mtlz&{433$>{ZaddSbzapOu4L^*uP&-$NoIe6R& zT?B9fn!{sOWSJTro0s?EXmPEciG7kn*$GUy!UEY9he3`do1AL`Cq>kqXeouF!0e(@ z1|b%6&HWqdYpiMYCvd7)mh)HCkuL=>T?$YL$~5z3Xm$C58!6!Mxva~VmH$Sym)3Ab za&NY0kdm2y1WdbHn?B_wg_7QIA#a?r%50`fTujWy4E0nnncc}@L)Ln88cuDkT5@Wd zo(WtNQBD>RFgW-+WHS32gHi1Co8HqC=k_81>Sh?l?UGkhZEKMJYbph-T&92oCQQ2Mlz0Qkn2g<7q@zczmfvX zbW=Xt0`Btr5dHpPK0HrQgkP_ z;MgTcJXVeO;^VY@QT2CuF`zpM*&`9?Mb4Iymr#>1oLk%lgwzo=U>xy>2*|K(c>Nsju+)fxXaeZcW|52D(oJJbx}f^lb_kD+D%NG0_ZM2yf> z6~Y^#;UDcJ@RJN(*V$3Uv!fz;PTJlM4bE*Gy-t3lRkm9k({#>SGO9dOQDeSRlX?iB z#A!^f6_&_mpxZEPy!QtJ4@e{`LTL@e#yHE=@s@cvZe@r{U#YR=b7WzX#0BfHoTXe# zqwtj;A~iz~U9!i^6dZ~`X5)e}wT-N8R7ePH9F#u0U<+OfLVqTBf93iB ziBHPwEYF-Vnr8S5>1ZRaee*>eNg0NiDF%I0n{^A0PAW={38K=(?1ev=*^{AUFrT8y zTt0~MFILw!&o{@ED)~&G+{e*p^3-T}1J8s1VX)E-FHe&cZz9K;STSDQoEO;Q73vgo za*dx+usnAQQSXNJI0|5Ef;mkTr@)qpiNT@BVT?9)$6#GB84+0%?EI+bbIUw&-WZA3 zL{>B{$v22I8V9IiE2WeJ`PcIV_>WcAxBz`H!DZ6F;WAX4G1SDf30!;`G&X#-)UbMI zf7UTMG3fDYKM#ymC=otJ;O!?ao+b4!<0K4l>C*Q(RB#`IlnmJ@hHciWPCj7vy6jR^ zf6dY`Nn2>hG+u8^Jx^_x)n$ZH7RC>*n&+~&cX0a;nPMv`h5}rx&PM98xT=Z3haRL1 zGbFA4`DzWszjoMcJpi;+(as#}G2W}$3G(G(Kna3*>rGW=$ za;FO^n*wWR=F{mt=voD@Xf|;MVk<(o-&EI5y4VIwg?$=_rcr(jS5Nn25G$I&GyGeh zu#&t|0yjUR(w5*A@)DdD*eSyRAf9 zh%hnd=ph%UH8|SxnI6^0hOl9s+ZQ~2AtaY^scE}0n0@r*o)Vd1YxnH>AVS@|;D~v%Gj_ zkfH#y>&anz0|eY1y0|>xzhE3;4WrW81_i&Jr(Tn?_!pGwa!%#k zFVUekt?LQhsb>~j7Dio7EY>8 z)TODeb~T=0B!{+%X5(|O06gglP;YRd`${y|fUGxq*u$CJQ`DmF)6`5qXNW~c)^klN zG@Nh&2D}Ge89LwS8+qWclbKeh?uCtG1^lB)m2oN&%i4vM^*MAPa}m#AB^HX@ORDIcbg6W z3-@jNNscyZ8%1a**U5`3wiTIGH~tA;o_FBr6$svoI2~FP-}8-9W^*yRVp}nWTvue@ zMoBX-{xQb_y0^C8tkHKBsFphwU6<(tgrIbaf&O$RgCdot!s)oBlRB$WZCH}Mo~I7N z3}e7&-o#Idp~1H}xOtG~RT+N`x0tsZTq@jeUuV4f>v|jm z2}T1mt|Qccnmf!gs1{)PvRv!3Jm*%ysG*9Y45QYg z$`$mdD+Deo6ahoY$O6r%s9a`F2N)-AwVL-Tt8Q87T{NO{qt+a#>3Y`H`b+2%ARQ|c z+Lkx(jZ)Pybcb@4Juq5yM3-8Nv8MaEq`nKTu7R^$$T2!Ek4g#pu`a%j7+IGnEki%a%w<4-Dba0UV?@#e#K<+5WVq( zsQB-ix@iAc9tXLzKX&NuuZiNks#i3nzO0wIUlAWZT-!lO#o*h@;G+A6e& zT99ioE)PLI0InE$iCq{Z5?}^-(t7m9CDCyp+jy6v==-kvL#SkUHazwJpLyRL5jq+y` z%I*-)T|QS4#Lzd7AHQw&l-~>Nq?b!oe2Og>x*ABb3z>-xsK(T#Fl#P}>+)!4TEl2_ z{IUrl7xM8CSDZA1;fC>Ts?F;1CrfwXqv`1Ej<7d96b#DHa_8MA%_+v+&_wAa_?IdB* zHbctzF*Y=!95G{C>UKl&*ka*%BxZf^={Xf9bGYTlstFP$#)M>gGn**EUU(@p6ke=z zw-Q-AO4`FXJ@+ti=#Iex(%21ROAV=7_xB)vSz$1Z#IMKs+W5 zwrBM>Rph8DXj-8BRv#hKwKRu%QG z{&S)IRc!dkQuJpJZS(UP`s5cI-N2(kl8@Xw|0#(^jU#jP`{!WFcT!WvTY&(qi*mR% zahw?2^yrEWbcx!5&2o=pDB_S02JU!@$VUy zDl~rZzY^OvJ<+?}N1WpV`9+3bC3ElU25UxwL*h|fH`w9q*JBuB;HzGmt`p6aTR*Pui#8~tz(kH!E?d{yhJCfrUrT?It!Rau4r z)`GX>xIE?K9*{bJw)o&qh=Qil7S z5Hd&h^utA8fW$AOqIp?!D9xlJ4ja-5jC5&j6l9V_JiJGTbU0GErQ?txZMMltDQ>CDBsHBEOIHyEGzw&C)9kiG^L4j=&&kZ*EjFMpAFVvCvNSmOTjsFi+!2KUl+h6 zxdPcI$g=$1Gy>o3DZv|m;qTGNfZ%u-c0wERj$1w@Hu*W5y2DV$*pt%QExV+#a*heU8Lkr z%Qa6}q<)1YAN`m@#^mS=sz|tp&%2Iy$aNeAP~)23LN1-s;0h0^XqTEVj34*las8AErYFT~ z6HYDGNCE^7zmNis+0F{g3>rb}a=L1$?yJl|E=g4$$-st&We}ReWBWZgc*^n~>}CR2 zvszc#UDw1|UpV+{)%Kr(6T`MZ!+PeF zPh>TF6kgA?dGh+nx4c24-T!3#1#Dbe30`YxJk7{$zs;y`q>Pk}rmGvDdCtAbXlNv# z_>^#|GQFb`tM9aqgVM@fZ?7T-sVa%Y)E}_G1{J7bC0j3$D>+kGc;?%l0>n&WtyfRvc*9wQsBe7zU}(k8ETlJHT90pzE`U z43t==4l9wm8AfW`2DT_!wr5CG8tvIESvY0OPvi1CZs9gbTe2+eSscExwABVw;36Wz zaxP_3d{R=80UKgogGJEd|uwh zUf=zj<8i<{;OGDzvG2*vsedBN;#B&vKI&*d9<_^h@*AHw$Yy z0xtJ#Vrfm(CNp?{Mio|K`^@?jPx^IT?6_y($$%Ly3H!~~?0X3j(~epH8^iXs6Pu(f zEwCjK*p3Ja2}h6JPx1DqB1AGRM_N&^)lO7gyCS}TBh^Zb`9gw~b8Z^m)eQp;VVbD) z^y}jfP%Cv%`@VnjyvK42;F?nN!C)((f3pE%0~F}~@?St5N80ovv7(MYV+5`rIQcO* zD{7Ct40wrHSxY#b|F>%Co^oK0UIhQZc|Mm#<#hVH#05{2`&>kRYQEM&D~ScRIwy8H zf06^ia4SK`GscIzW1xN}_b$g5{_ncoX4`#l-*Wj;ktD*z&&6U$<9)FGWx&{Wt7!3o zTPS12D@Pr*U1T65l_m}Rxa-1GkPj`F1#~MmRKPnJjBHT}_=80WtySS4`2taYM#k;{ zNvV-V>IXpq|3^V$MJo`{vQK`S=88}KuNjM?X0TadywW)Kv@L7uYZFi6_R9DfsZRkT zZ(fc9hRdS1^+`w*eG@JU49BSA;0UuYI~X3~M8n(RoA3^>0X-xR5fD7ojRuzCPv7`V zuo)Dxu)NC+wIFE8tV{uY^3v=PZbC6JE+<+SlNtarf3;gUunAU|}m)*0xX6KV9l*y_Q>v{m$P?0Lh+xCX^r z%2?mOb-NUqbC7r_Z(&%m_F0ke~P(B*HyzrONc5wu5 zLcF$oT_H&mWPl5}QMxf%)Fyjs(M38X6ezobRZzJdb>NOH73F}bm zilK#?SOVzJ_{|Y0EUm#*$EFRVbp!k&(mvG#oR$Y7wbVV|Dq`;X33(!3GSN!&&PJjQ z@k_}_8jUSwRf}c$#R?rKx5Bf-DXwh0Q-Viv(JAta#WHublx->gyD7+sjP_>%^-6rN z>5be1fcwlE(f%^=I#rxN2b>ig4LMEO4P6EC$MI|8Bb)@tgvHVv_PXloosz1+_us#t zB39X2zh$q>uHA)N$>r%$iHo%C=G326`jr(sf=aFxW;n$&poAf7X0qyY#Q}}!B zZhpj?>`jhSPHN2j`*SX17W#MgYAk)F9M#65c9%U4Tm+CqMetFcP!gu8xB^<+bF(Lj zGJ79M)BJkktfUcOl^*I<(!-MN*=npjUP9%k#E8qeD%E44ic{I8iH;=9q^w$Kohp(S zVSIKhS^KR)^V86lm})4@tB~iYWa3Dbsm`D>srUO5>g~ zd=92}~$u&AMga6A|xT_0G$rV2d_LkX-qQx;R^j&jtFV^gKn_ zT%u?Gy?exB-2dj{DWrtOUsRI;U086R(mh?1z#(rU8Cm2o+toxPmVN%&bCVWSA>ZyW2Y z++{EWxemz&L@d!J3X^A<9oIujZH<`aq#yfqd~Uu+(TKLD{B-OIup_vf;}`c5q*JWs zv;DJn>q6&LpJ|H6qXG{<`J7siuQkJcDs4=i3S7Jh5Y9X}A9uufzAp)gOf<4+v+m60 zgF*7fl!7+0G0vwHq!Jt5U;E+!(QO8Zq}7j}_O)yjHmPY%k$oF?KY6^G9O%e)E2s6I ztHsrB>q@eba&}CW`_^!5)6CMZ3$z>(Kvv#Zlk;W>2AfT#PIm3aVOX?~zRmCvGykVF zUaZoXS!JjaZd0|3rQwAeL0hPus%hc)Agl;#Zit6p*@tGA;*OMKs+mVlyonQX?v>l7 z3kEzqv?^&7{2D0%miO}-E@Z+@!!@}NMqsv`w9876D0K^LLyJ|*frq8E^Q09Og7ei7@R~< z^gu(*)0HFpM-9S@ktC9$*`QEv$O1XN{Ds6Dw~9iIzt@M--Ug^J;p)!}3{TTj9R?aN zjgPF0Bn1$N7ugi}>|rQkH$ufuHph$N@vr4c$bYlv{}5#UF+MqqY0pOg2P83V<%S9~ zyv&Dxo)^V02KogS`|Of563?EE`A_Ir{fvQ@E^Am09-l50jWUxy5(yXC)bHEK2&;jA z{9=th%)uqv9}fzM8A)57!uF=N((zdZhn@oE!)iaka+uT-9Y?B3$&q%Bfc@++HqJ>* z2yH>221Vgc3-7|q_ovpdjsX8-F$D&p@(y9?;5KPFP=XSWpPdXbfjhPV2s|7bx48ld9DK&q z713ThkBFJfXD0#?`UILC_I5tG+=yp7Mw#-+3KOnlDkNNa<%lPChDNyi{DAsxI)O2E z@*!OwfBPc@dwQq?U8+f8WM08l%Z>#2(n~D5e5~B+;<`yHE=`jB7)?+X%E*Y6D=Y!q zz4P)E5I{GqvaK_9x;rzz>U%(gdvGSYA>tMlXQO`7=o97q1b)S=?Wl<@#@SSWGeHwA z;mn`w{o>KgXd6ow^XVnuiibD6RKl|{$lETSO{H>?Lw0t4u0v6bkz0#+YCFmwZj7xn zC#l-MYY6ob;sx-N58Wflw+&7-Je6LdF#W;3gCA(ZG6?By}U2NNDUjYG@g zxdZX`jPA8H&Yhp^U=nNyQqNL-Te@mz*d%l~l0PdUIist$5q#s~9mD_V<_&Anu8}Xb zcm`vik(F(iM!9l*M5uEUiY+2eDQd{fg6nfn_JQo>oUKP%;tp@Zo|9(MSuk)3v92w{ zgJ>NrvwD*PcSW`CK39alo$M70K575+dQE?_RL)4T!mK7655@Os;|WcF+VvLuz)vl zJ<%CM`Mq;2WE)Kp%+Q}_qiFO_7HXKx>J?t);k^cBA5)&kwgtv~cv&H%Vsfk`2VIo| zW1VB8X|$OD<1Du&3;z+(EM2p*yK{5AW>n3^-6#|_0&~Ht!G)Mue|wSt<3-Biz|`r= zoD$G|walUQZ+L`Q;5--yMuRAkUMOh>Z97P*M+7aPmHc7Ej?*AIvH^4C((Ow} z@?Nm#-X3uM&d40*D@?D#=Bh#~F{`x^JI|^?9v+#R2U(wv8~r`B){ULr;+lcOl6=t0t zl&F0hYTsV)eO$aUkj zVYa;LrPE*UGq&p15g$dMk0Y(D;!3lefs+3yTx}e4>vv98<+Q0`Mf*?|+~}`r*natEHoIBKP+8 zRK|PQSFYR+pF#3E%mR4ktspWT-HqG23mdE~3s*25O#wcl*r_YRE5_(?s1irxhW+k` z`5PkCnZ%a(3lH?B*#*mIhO0aaxN18lY(|3c^#GDNN$1v7o7b41ZEIA_B*ZFicvqId zFAURRSIt@lNMiNRY*V4NlG^@2G7$6FFO-tJ^He+*FRWd#!=amq8rJd;nRk&$_$bF~ zye4S|n?ZWoD2|7nS_!_j!Tc^hi-b5D-fvjw5iGW}(!1tH^Vz;xUxpKbgHu;}pDSch zjEDG*x=Ut%ZYfUuZ7u}A5udi)3o{EYe61mTxKPkiAo`;3k)_eHuWy$x@JDYt5=#}3 z!}QqUn%4;Y>;woBAXgUlYV-npLXF|!#y<^(hL1MhF#xMP4Q8huQaOgUp-E*V78+(>hnmC_;Kv-zP z8G4=otFw^CB_G0~`Gt4kmm1$?aQg3S{ueg+I7X69jhum~KYey1a<1q)3vn_8uHyu9 z*al1{e)8zW=R)q$xo2ns?*4DD@nY~`h-}fBEU95JWPzy z=@9nw!3WyZbd)w(e{W!IAXuK~3aQ0bDT-4yjhJa4^^IK8N!TPH4WP?EO9QHe_#|Kq z)SsxnuIUnEPblIG9P4D7CylFUrqGMtQ-N`+v-e#_yFgF96;q{FYV$`E$SN3cO^@u(VZCR7+WsiQc&iZ#reAV(vi=+^%Td%R zZugz)KGlE92=Fa)eM`8SNUTpSip=*oHl!=li62qU99nlIKzi3*J4%`^RQE~`CI@^p4|IVJ`U)?3HC=rC>VP*8$n47x3T%G7Z-6}CutsBFB9=zG?97;l*!qm6LOe;NZC2esmN72_#Q}O$XqX>d zegJm@6`Vxg4+43rh(7w(my<6CoslJ~^K0_ldD5ep-pwkIId6Dd7j7|NjC#rRMDoz7 zHzQx^)HF~25fqR$R`J5YH!XK{MjY!!JmTMuuL2R6T4b5P5p#$W8Lu*!y-Pv8oGq$E zP!-lex+)c3laE%|#TaSMnSl|)AcKJXKjhbh(yQ`Xg`M#dFHTA3c}|7U$pC^EtXwc8 zBMHk{1}h=YiY0jE3J3e-hemvi8b0oC5Y@@2<-YD?odf~Wz4K(Cf#e_>M8V=4CyB9y z$L0qe-hH944^}-ustaMrcV48$S}bFm92w!oLo-? zz)hXZD_7C{rmS?JqdA?)tC8L(Op(-$^pY-GTOUr;Gg2oWGykXibX>E>z9`~6snW-% zgH%1CmzB#YGY;qKwzB;E#2 zcWbqdh%2_?k0%ubAB=5(!)a}O6EW5oxl)jMD?+4Gng|Dw6ozKvRitBZaEA)1wiuk7Qs^XaQhH9V1CdT<=Wt!6O$KR@# zwbkZ39+ZrzS@7I|f6MFdQMm&~Z8zz4je);^Iu^t2`kFKNJTQIhzyyRK3f&$=o`~wocX15O_dftRK*qn2NIPFiUC2!nFG%Z$XtBQa{xWS&RC^#R zh$LR=g7tw|Ss{22wc5pl2B%7;cHt}C6OY>FS1ONHfRfT*0x{K@4^g5%gqifAeXe!7 zb;0Bh8r9R)U^rE!@$C4DBbZ7;t@BG64-l16-{h*D&-mIWsHv%r$--OX%BDw z{67Y+-hBxY#O36c=z5mqDg%%!yTmm<;SF+;1p%Dijt3yX>XSyfC+MGfK11#APg$9M z(y#7Wt^xkqe`Ww10s$qs;$V6Hsh|>j0n#}ob4Yod zGY`?x<1+)@ZE%vrI1=hOW-2-L@BGH6!N&aOY?vG5M*SuzKGix12r!LB^W;3PH|caQ z@S2d|l2@QhDI;B8OO#Rjy_N1^8LTEFJY!M_RIBhsb@DTs>g0Cg!DEvmaTVaLQE6US8O7EPM-4kBlda z;Ka%s-tD!l`$OAkZ(Byrv-3Fd2~EfW{3W_Z3W3l}W)_jVlclt74z~kM)P?iK(TTw{ z%}c3ZCK~ACcLJPv#PMSr$aP7N_+d5=f^kE(Qz3?l=@DLgS zUC{4nP=T(XYJ?9eu(9jZ6HEdE1LP4fcfb~7;txu%wqeBT$b4SBo+<-&FV*9#-Bk4 zB@Uk)6+y@1&YbBKeeY8MxIvP}`J;AEy=wwAFbr`5=t}}_FQJkWisWWS2Q>6Kjz~Q< zW|6QLnCMOaFT6;EoUkb_S0C!W6izDWXozQAwGMZt{53otq7_XMOw-;;u77E2N%cXP zqPB(cM#xU{;f}T*kU3l&N*m|J4vU*Y$)*C%D`dHjHfQeU$GLlOQQ^|J5kn+YP5Zmq z{o2%-XA#FvWNlozWg~igW=Oy*wUg3pvgy2ACCKE12F~wSPNyO- z>wA0Q)#T(tndxsn7liW252bTAXk5!MKI=U*z+DO?@`G-P`Ln**UL5=ib#?>-dZCq| zX}Zv5`#~CS(H23-D*=G9?^Wmp?+A=mi!2rkkP)r225o>iKe^Z65?h4_;8AB>*Q)^B z^pBZ<23aT)AqwT{@Q-}#JrDFel0s2^Ci1ORPts*Xm>UG*Q8ue?L(tSIHoOrvrFyqA zr&DrJQVMC|W+DvpxDJI$7z=V!{UFSbQV_^qs0W-L#&da zlzWrMG70s9N~>$=!j1bILrNuMFF(vO94-sYdF&LZRe^su8Tjv3ZvyA$2=*mo?qQvj zSNB_?wz8lk1S=Dd;g}=TEwytmMi4qq4{r%06+nH_^e1CzN3Ng!Hv0FVZ;a&sMi-i7YRm;AKYME2I^P^i6(@ej10!`t`mVbNi^l$D z6&tk-0l|8~?&G_A=+=}X47Yv2U4*h3wgX!!aJX9TQ7=9xc)#d8M&B)T%)G`tC3ZYu zV2a2fP(g!}>3MDEf%F0FWEEG|W+7pirj0<7C8Yz9Osi&txtUnYddXy+)JW@lcU-PO zr>2@b9q)yPQgmxTbrU59ijtF>$`MKd3viYlXIT}Z1ZTWpSE)8|N>whi7qR_hZY@Ksa5VRFpGX^dVKP9@~I&K(az+>gMeE7eEC*x zD~CxAI`w%e@IGw0mO9FGZ(!wK71z2+zpq z@LWG=Ez3HYv`aPgM#nH1oiYednux!yS{+CiJX9?x97_bhUZ;~2!Y|%ZN zLk7|(wyDgy*yvfJkf>RHNYgfi8&Uq+$n^RNa)fSkHL(JddXONwTGc_h##~34vZx)q zpSOR;pb@`(kRKhFd9rbFKTm=;67Vr*lcBaVbFA@rNAI~ynjCRcZoUa_@ze3^oeHvJ zN656}$sf1Q7q9xT?|RpRcEG+tSY{>qx_TewqipY9(VO9v^ovDMI&ao2UU)mRqTYVL zHl+4fGwa~H0aaLJm&jtzr_d~jt%f1AJCC~Jf*@JU!00`Y%_^I=m>Ze4BgV9v{pyTC zfpOBiJ)OrhDKPxc5Wz~2qFR-R`AG`Q?F}K!)x%^TqsF?q?FKyiQxBZG5GA0#X!S!6 z{<4<#&p*V)eYOU^matkNR}8CiMN8U+SM(B^W&0&wBdE;lWLk@6AllxBxwto!V~i#W za)(*ZOFbSr&#|c%8N3k`uuFv5@uhukBgxYNg>%+AuKxyU{61b>=c}~YlXL^YUcAwm zcAhp-R-YBdn3*4%qn^ueg;Zw-Cr}kJipq=ZvYgs6(v_ARbKSHroCwsKtvS^~W|(__$9-N}^g`!aB^;-x`I^dq}E4YMy; zO2@Gab(x0i)fq?W8BHfmlQx4D5C;f5uZW~}V%kk+LaW|Zgy#vzIy%6{_4$~vd9>wPJuEmd{PGBKRe@1CqgZvWo(+_;Enb7lNA=T^fk z)BEPG3&o9jiIWOJ^g!wbO_#6WrMHzbKaC}~8~^Wh%@S0l^8}0(_vl0_+u>yrmJR{a z&>K$nsmXH)uKK0 zayXKq@E~R!(;m zp*|LWCl~F{L0`WYzs^vQxEw9YJ|mia)HUo|{f(V*aKrL&cq}ZO<;he^Y$R?&FFuSA zmQ0}rqO;iCoii|P&FWBV*RmuYFaUFiozbH(! zM6+&Zv0kZR$J>MVhAh?7l5+?dxHaJ|!Lo}Ht(pXI2pI(PGp#Q^g|3S_IFTC8p^-fl zb67eB$(;T2ME@p@U=Xr^$N|;XUR_^5NsG`{ z&`q^x{{RHA)|eJWcuDq!?I>ZYGtR*0rdn>Y2^rdxDU{`VQb^tGBC^6Wu4>2kG5MVJ zTv0PqB5$DT<0x%P;G5NFQSDxf4BTkR5ci^HIidIgVro%nYnXk{os4(n`X~j)eLNOk zZ(pN~W9WBX;Up%&83Pu30R2>RVgf1zyP`WklvCTS2H8qCKTkY#9%JlH;)lQ0Dp`@X zG8vATT;2g?IC{Ih2Plc*x&J;jlsE7j`Wl!~4M$0ZZcBHnZ3dtQzBw#(10$}VJCi8D z@1z#ido?+i-V;aI{`5AbIi@5+R@rq~Ur!di)|!2Y^0MYdjKbNI8!~%VC^rSMA%WB@ z4Up?G30iYU!glC%>MH{7oj_dRytcBf8F6`Y4ql@k;TGP9+0RN2t)>zlso+Mzm4Ii9D92hTnZ#OE7m4)1gb;UErr zQ*+97$;A|x8*@uYa>^wW6f&j(wBFRH>nqRGuYXcP;oXn(w6C8gV+jTA`ts^yo>%kN z-Hur#}^|JuZWy5e4Lz(!JHpI}ZZBJT<*XmBbs24U=~f5uKP zg3N)rLjoBeOy;^evHJ?;%j^NM%K1ewvUjm^BV3m%X}3mUk4IwUOJEV2We6>Tu#EYu zqccHEdG<>LJ736^t(OW*Z_l6Yr_|Mx{#^+{7D@A}?OBmLJB4GGu(5ulq86jY*bq`6&ST;h`1PoQSVL?am>ulo%inb z=zwT_1b48v_?#i5@vNb=N1-DPyBdggZ7%0(vf128b~`PMeqtVsp3CN2bz3jAD&5~M zftlIzUdXMGW2coXP`6Xmz`Jd;z+SGp{dHiE45X#KE~Z^|s>5?#?4nE#%AUkXEM@c? z+@yF}(DX>w-ck_P@)}@eM^GQ*>n2?oC`V#`BIJOHURE^C6|iv&|5J(uz>eBzbh57p zRum2(hcL^(2l~PGwc9tbfqy6`dQx{;JHyIxxgt!$FF3n7Siw;)q8OeTDEm8 zWT<6VtQ4qxsg?}0Xe`94u91B=WW*w!zZVf46*JUKos!mDUbI`7Q8~{}igJeN0`>@7 zSg&s3)-ydKwv``g56%fU#6sVqr}agDWlB92#%c3<8>Ox6My6NFDG;gsHZT6G^)3c! zlcN<%P=XPVwLT1={MiFa^@q910Mezs{xsI5BbZXL#sj%O2(INB#SCA8w7S}w+#*JK z21_SPDTbI$fpF{5YM z@_S;{uiEVnZ61t2#xFxL7YF?OPz+1Cyy6bQi|Nluj;O9fmp@b$T~`0pAP#GD}>Ag++_@WK5~~gw|8zYB{1VR)EwKad@x8&`ot(Ruf$& zQkhsr2x?S*S_tDuXwb)vT|6?k+@C9l47l;GgD=!Ha=P7)+X z(Ltn+c)`*|#bk5j3{2`~C8#nE(6kIbZvKo|C%e^8dfe^l!x9X^%%mveDjieKJ~%<6 z4yt=HO2W<>@^L8>_5iNE5*|(BhPA5Iuv&>FgqO%8M5z&0!V6-)l4=syX)96E)KXuL zh89_dkrtD5W#j{j1$x_|Xi;F7KCfSUpdwNC;!wlkkc|Gz5qS&zSMvzg_B9_LjcOwc zmvQoMFK<-?k#a}kx@T=HQ`}UNP9sdu*>Fs+i!tVC%1>w-&@^=Te>Qe-p!9Uc1ONQa zA!2b%&wS>h|HFm`$>(EF4wNotI;D4f?tgkLI2}5#p@#cv`L^kny^@O_DK)Qp6sZ!) zH21=Ynq&Z8NO%qG?JS53VjdVGWKX@EG4ZyS;-*vzSwikWMc+37Z?Ejv1S;%40J z8wBF9PA|poQ|HHYH4p2?xs|(|Z;#AZ|CH5i`z5!&ZPOUNoX>t$?INp{Pv5^|)4Fc^ zRbzYh96|HJXvU%Lr`0^K-Lv);f9v76cgiiQ`OA=v#iElZ(x30Ee9fJ?<#n9z>;KIG zdlW6HK-z61toWFEHi4h_ssL6Jv%M;5j7wuWR%s~j{g5Gdj(gyr^387}1@EphqTiPI z^LGEb!`nY`5sdQxziW2g2is?i?+UXQmVi7z#UK3E`ghGv+x^IBLLSJNFHU`no1bZ! zXa78P?)#$m*JLT-hxJ! zKK_rCArkj@yv4oMp8WL%i&aT(u{?dubrnT5>NC0$w#wwU zZxgS-`^KC-tvQF+b?k9iJbBcaTfMuj&tIIvdF z)Icb=mv%5do_a8{xZfU!HmxYCLIPdyW17ys8n5Yfk7VXtPp#*iKxIk1~T> z45_?$XKbnNP-E9+q?95{4;(Q6X<+a2-XzAP3O&T) zoLa$_);Q~$TyIhm^Cf<2BY2YKm*9DG1q)rT2TUIkiUAe*v9>+GN_NTfnhm?+qgNl# z34R9-mqG--9X;uLCBphcrb26Nr8y(*_uUv6JJ{QjUNd~6w%3j$7u%p@Xxxr?Cm3*w z&TsO#Ug%o`40%-4a7gJSGG%zcj#T`aD0+pH)Bc&??K*aQ0 zpRDHps7*h1oR+3GpX>&w-OO>DKhC_A%7W#mgJXAHMM{{3kfs$=9yhqgh+{SuJ7=ho z1jF(igb#|jAFkbadbi$8Asy?zJyB3?>O!z%bR|XGk?&J6ixo_`TPBGjWl+U35K*4> zQW0HUtgDrz!BQ_p;`U?MZ~!;{-tQ&@-v{vp9q$(2{?C!Y|5m8q&S^-?i+>a7N>9+? zrdZxP8^!+|k5b`mM6gIdWWJ9dGppjgjT*S@Af1aY8lj z61Q=N5%29}A9R$R)~h9U{mY|T{M(L@wOYtFJ%Ow7YN3V`K`x_)+uz&Cw)7U`5r3aW zKlEuolUl?r;8eMx2hj=A*J{mN^WT3#Q3&6ey{(DD9Pd+TAc+i-`;c)E%CI-CP6|2cOh9QJ}52Pr4`ey$qM>d_jFk! z-I>ozySz2VnBOok-9m&HPucX@+EVtfAUpCdRT`509I&xHh>mC&&#{%AU9H_!E!LkH zsyIf@Y(B3q8^j%HlsMjH)q3yh;1J}GNMS$t2(cP1Hp-Wvi`1$@q)-^aDmCaRPXsTL zsUsLbIU|nD|B)5WXdCFrYPQI87%ksN;dL-{x=w~Kgh-Avy|c`9Dkzj?%`^*mnFce{1e7&d zh8JqDS)k7RE+>{;^Enz$5s)!fMw&Oo7+FZjlt(gBGK?YKOe3)Mrgk9pm}<<%2!%ct zsh#rWMYg1^6%K11nE>8qn<2cdtrTGOTvpIoVO%wY%XG5X#qErtLT<1y)nsTSYwd)_ zP^h_5&ae_yY>Jk105PfN+-~1r^*{Yb503Ocw+U#xV5ioe)e*f9tO5Wu5&454LG~I| z(IxYUq$5UQYSHKHKc;QT#RMaJlS%geknXI5M0aMD&bX*?W{rkfmvz`89v@q*)fEy1 z{3t=A!xr;-*dmQCisu&UyII=o88sTu5Dav)_4?dkp!tyucC&z!D8s7#@FY>nS>&*s z0DDoZ38#m16|LD#c2;&6JJz;|Dt7W(G_t%BRbfk2KnuS#q5tFks1&=6>SVy~3`91h ze4t4K1S(Ry9J7B-T8DT}bWAxdXm zjBuDaCdi9E*!$uew{z21ZjruI@(P&yh1Kg~qOt)xRGY_Fa@JCObe3HbTk0laYBihx zk||SfQMxiT{*9}|c{?e3`?dVANPz_hRiww1v8V1MmmRi9H*Qo^S}d1}HVieW*4raw zV`#`aB%m%JLRt@jW65iP?D)SucQXz@)Q1c&eD)k?!v6yH>eVoC13Y%s;pF{k`|I{A zl*cxCr-u2z*MIt~s%9=vXzO#0DIHLbN!?nXbjq_}TZ;P5*jl%>zk8$hE%dp`!2f9A zq;6i#?XmhbQXT!*H>%>?dRju4paxFLvq$K64xexBZcgUKl%W4mc!H#n1L z#tTEl2nGIgn(C$BVFYxEV>NFYP91$nTR9`VtW|VrY_INrmXzawOldpzH~T);FvpF1 zYVeTXX#!36>iNn z^42`oTZ!=1&IG>w{sK(>sC?x)r;Ok1y6@|>GPWNFO8xvbR=&kf%2ttVn>KaiRH;+8 zmiyXetmgWpR-q;`Qq<588w~2UK_zxMuDoNbV+-la!G7Fjyw9|L8bFxqXh-KqhjlA; zkFOt}s+_Vmc@@}t7F|IwmphZ%QrJQc$?Bo}B|^#QP#yQ8vY?d+iU$-tRue0XQF6*) zu*Izjj;#e-NWhcvZkFWYkP7b>2*vokC?t_vwj(z;eaF4giMZ?^clcCQ5S(?p@hNse z=Q7(k;M<*_?Qi~eZ#vtkG={}aW+oeSJJtNuRU7t)=!j{U1+*tX{DD<2WjFBmVbcZK zZ<=+gF`t!6O~4xeX+q1Z#T6II9)I{!1+>4z@6#x1_Gcd=|!MO zz7K?hO+DTm&)XE9OI0z7z+r?Bj4gmouXq7SK45<`rMqPQddjVR!FOw)Ds{`6Lu=ym zdu^Z}?EvvEIl#j8bIFH3eADj!re+Z=Bfgp6DLJy{rGcPo_lK`5OTL%f0^5jh=Xc-y zQSvhW@1(WdtK&j@hwptKm}H|$f2dwaI#dbH5kY0r#g9Pp+?M%nuZm5F5`12b>?^D?&Q(?dwCpmf#|+7jAOvqNIy)ss&6Bx?qNtlb(znC# zs|}e$j7bo+vjTl7-$ajkEb6CX)(EL7j}E^c7oA9;{Tu-s272T3XBtVf&t^|G4$cE5 z!*7>OrX%p}(nzeBcfI^6@^W}}xz;abcy?EJf+EkDOkw7G zl*%IPuQ;_aXh3{vZXr2|X?>)##lPwox70&cI8X$xWfPG$m9!WmlX-|WM$-^SrnkRR zRWn=!mbT~aGAVZ&qC?#oV~%pBbexGQ-ej=hD4~8H7ZFxy3yHsO#vq;g=c>l6M~Ax; z@2aX@#?w>sk5D`l(ys#Eb9@{P?vcqOSSeYP-n{-NtbCuvmZapiC%X8(wsCK!{pRoW z;%FTFTMuH+b&tlQ;r{O!r)q#x#zodCRueNe?rE&I*0Z*A;k|(@-CHH8A7d^MJO||= z9}Ax8v;zli_MPf!afhY0f1#X6(`Vja1PGWh#`s`3MVnqSTt_*{4hRm9yGfT_*&NDo z9#tO>e~OLNTD2@KAU;OllGzoU+)^TCU>bHymCL487ooo1*yBcLfACvdT&7~F6~G%~ z53__uO(908ZxH@$!HvACs=}yC%qs!6{US`7(7qA`2`0ZTP~EECp;J^0r8dHnR;*)@ z)Ono(ouUM*)e=j#t7(#)6^TJM;JT=Y@UkHE`?ojZmHGzxZ|zl>uby-h`TZ9!1Yv~R zVAg#h@wOoB`=2l4725jW?{U+rF`r{HOq8#GycUEKUj`!9rL^$=Il({Z4}zc2|D>m9 zwqkrt<>yE7JgH(FIho$8b{*5d_O3Sk#Or(Q&57)lPgLt6E+bPC?IoLiumP5BG5Le> zu;LHUU&j?khQhr14+`!De75)zb(fWq+0F^5(yIAV`9xwWFGvR}6VH#<&eP+|#vWEP zN}n$eLiWn*0)aYtFH&88-da0Cn*+IcCgNuY8#8lN_T3Y-_QIgH&Bcrng2BX`(oSik zTDIFpA1`cc4=Ua~Mu+1km1YMTGjsfnw6ze|zPaeD!pCQoc1RnBws+w8y*m}v>Wv9W>s!# z$1R?s*epF9l(X+$TT({V#QS>eP31wYlZ$LkhnD8LAC6tOsm`3#yO&+Hxed3--v)5G z`ySem(yPbcJGh^>F&&P}b3GirVN-P0tC2pc+J`l2G2f_jY;dpH0p8a(-F9~U>@*-9 zb~e4L$pTKvH58n9a-yJq_2DOn3jnF=^J!pCdQoUS zcK33uADvFkbMAPh234K~uAnx}7rNw)`PfXLJU;B2B537_OeN%8S3jiZd8+z~09zQZ zvSGNWqMC3NzC<7ScoNJOZJ`GkRGYCRYCa!hEE~NYalo`NVUXYg%ZC)}fdLj1<($o6 zHYcp+A27=8!P=I!>?@?IOF{=O%aNj^x@d}sIPt%<1)iuWni7$X6Qbm9`L+IX_R>;a zIME#0lqjW3aqaK27@gKS6d05d$!skw1i5w~2#l`FZUqpDpFjFYHOK;M4(5-KQ%z3!2%{0O6#)?h_{>+u!#@+|gVjwebUqBGd*Yze!T zOJdQa559(h!)?g+=}WpEuj^c2H#)IZf=(KTLV_~riDXLFdh7Jsm5C04npWqrtR|Su zT?DU1k*d*&3QBlQz2B^leaxY6z$+AWc)Q-7CWITn<#mgz{LW*cy6E9Akk8ZWJ5j_q z8l-94G0*if!!4$$hNz(pCD*r+omO&{gpCOZ9eNXH-Cz?<6#)>_a@}NH%h8MmmW8gC zXfR`H6lUfCPteDvF};~4%rAS$3D@6YbTau5`Ob2gkj+*qx!)|3V`O{>N{`1=A{%^$ z2Hu}#)R6RgRFGb%tyboma}6bKIibFwpp$CXRU&Lkeo3aHmNLKe&&Z@Z(UjbV7v9qi z@RDOEgTUq~Bz~qwo_>)u@2~w*ApGK+Po(l>e8GgB?PRpfeN@v%w+q3IYHGDfP3iQ6 zx;Zvu8{O_Db{g0z0f?92-qH6sJO)X83p}mqM9L{#Z}=;ZXcc5x-oA9L1RvDo^llBj z;sUbe(QplJR(IEj0p|lI4RV~ySk|J02sZX?YoNU3H7<7nFyaK-~3|1OhB)fvn>kxb6a zcG6{;mz`W8?3WUYqI5`M;yY6-GlgwHpLrw6JlNF7%k9^EoT7B@RC zKQ!^dl12cQ6sUFLnFwUR5Dl2~$s?K=V(s9owjxl3O-X;Al}nb+jtpu-wVTe=h=giG zM5y;%xC{OEpKL(zENjLcGSgyp0jq46MFYXKXcU2RL@ChcF@a!sVf}~iHcB7>Vr4c? znW=>7(E(Ufrg2slo9Tv|&{nu7lRI15**0zCnR0m=uU(f%E{vgn7G3qcVSnY1N3p-v zkSQbnp0L06*MsPS4HX{7T&`IqS3dvVML=^>7h8f>DBvO9QCA_4SHwE%X%tc@iqT6s z1V^b7h!To{(n2oId~8{vK=>b8W&>0;kO>`9)QOH?P7p5A@dZrg|F?#!(X!@Ixfrj^ zi}-;Bw2u5~_Jd0we{YZn-NU3Vinvm5ynXDPfKs1wy=7_2;mxbw&ET@`u@zUAZ~muI zaaNHMEy%kGv4d1}(f((HwCG9gOG;;?UVr;om&>vGiW4 zhAhY>rTmJ>=jJYl5u*qfF$CfgI+VInm6!KOdf#$~t%^J%JPC=F<_Y}HZR4DHn&Wb; zyiA2YpYG+P0Z*1F+fKnx64+$z!ac{nDMip|X^>-i{y;J@ddUH-WebQ!8V;o0{8xOC z3#xf&0J#;{T`|AxS|e=Nfuxo&>tCkXIvV1|Bd?$grXjb7eQOya;5TV3C^EUEM<-Fh zh6gcp6|kdvyc-zOyRCbue@DWWs**@qS)^w9y@G%fUg<@wkuZ2j0Rq@uU)nKw-lAQ~ zX6&civYJiX8n!PB1;B&nPo9kTdOQ?h1+AF^=t`H%WCS1>-)4@|<1rbo-?S1R=W>+; z?aPLeG)vH`Z%fZyT$wG6v&Qzh*~3K)h;rLWkg=C1242lzBF$S5Aw91fI5}ezuV#Ha zNoDNS$zx81{U_>PAynyr)F^WQlu`bie4)Pn1Q@0_q78?NuEcN$zi@P<8{ ze;xY{sBiD-Y{>fSWhesS@`dS{tWee^D;m@d(1S8T21K2&D<19Q^Z}~!OsBG%)Vc%8 z5PTRN1ysjAaYT6Mc=zXzkZLns$K_3?S4HX%QP;bP0Lzu0`l~(Z#Q`D5&tyI}UJy9x!68d? z-dh!PAA#ZOpm@E|&94~vO*Su# zSa!b0RVbG+P!A)tvr~UM9#?hb7mB$zlWFN=^CHXvY9OY@V6G+Vulk$M5Iee;W4HA3e1)_gt(Deb z)cO(loDU*4q)%Lb;L42Hd`NpkR;=8+7&&pkdEkTYz79*Ls#pe<%MdD{uOtRgX;#s%%G z_*1PXmtnCbS=NpDGBwrwU)yp)V~2e7mr-eKXLCUd>npe~D|bu>w5+GIvc=0gx}*zQ zI=_MPlgV|eo4TMQ`re4NtnAFTYgwPmQuN%y|w?cAX&4-kthGoKxi@{FQuW}eP9{`;8g0xIYJ_M z4A3C3sVCv^d`TxJQynx-y_;AIXFGc!jle-pEguT^p&tw&L)|%sQ0-_70@gaFMZ((V zhCz;PCx$C1Og(taHPLrDQgR5Qi8s0=;q=k_i&MmT1YAh*3Sa z%!{`qOZqYy{JdAGZb<+ppv|~wUd-DgfTnL6ek#S0UlBov`mdWgNMjH+6RpJH!Ky+F zn9eH#|8DA!0FTcnv#^z#m$gq-PI?zs52(J=29LO5oU>5|G_VsumwPNo7J$6hC`*t= zpC`fZzWzfDE1}#s>k~o74zx>6DUn?f`N^y}Tk^-q<9Pd0m!bnco=s(z%5vPLN%Ne3 zZ{J})=$-^HXq;O2X$86OpDd!7Uv7NWoU-XBj|#L zp2nf)Q{>7v4KOmf^0}Yj4(JCo-VQ76j&7p0~fB|_J z4rr`5H&X0 z{pK~bUms9{&Y>r^{}=%l-0O!u_PllfvuER50(Z*);b_8@=QmL83p4(BKE6WKJ;X=4 z-YZ-cjilT?TQt_l0kV}9y~zUSTEKiFz1R;0*!ZmF3z&pb`Y0nmdVXiYl3%)}C--}4 zXZ#0p;=3N_=#&PGQHc(5>2R}w+XnIMZL;>UqM&;y5P&ZdW&KwG;@lnfkB2-Uv9)SV z`NwBBfTQEvFc#Nw;**6Ru&A+_G>ab-k}5KP!+ixKR^iy(xy%lif%+frst#e?A-l6u z2*Zx~Nw;Or3jRlVy6*Jsb0;buk><{~)aW$pjQX!Df6Zsdoc0|?-d|{*Y4RiauhAtD z*rF4A?x@OTwSp1foxmRT*mDc-abg_@vRzH}{WPtj?4+xIg;RR&j>0Cbl6eVFThHUw zIU5zSO-Vz3FJ&i+~=l`CQkDF$tx$}Yerov3^_#5y*d zb6i}xmKBjg5S-ks4RavUo4*`MjH#q>p^8@^HF2?hv z&b|rmBkEYfMQ^i^BKd1#FYg@sjjhl_D;l@To@x40!xE^cC0K5&$aaQLsO88;QY@6n zb2mM^%oY*=*|FvKWe8_{jHD3dQ$7p(O++d*o649+@_Zb$^}zlSY?|Zaru39VeYlP4 zTrVvV^Y%?u>P!RTx;PLRbq~yNfkR;(bR!YqGmk7p>adSZBP8sUgh`JqocE|hi zh3}`$OStGrBK6k;%*P99K8zi^$qm|O_>L9Ql|=X@*9fawvD~^j`O+O)gwBF?QW?_z;SoRsMWmsQw{K!BSwT#V0AhU6VA`Y{bJ$eo2c|ga{lK z3uw|v+-p?azKop%JlKcp`&~CybYoQae5i1dTx^%|oKQyG3VYeJ{U*X$g^m9^9$uvze;Pf0XO^7`j zwadd8osz1?g;JOHg6g`R;K$R%9Hhmdt*HJ5aJ3N02)GEpeQtTbSA?}b5}}lpTs7={CW1*k=xx-R?n1FC=2^aF>f2WV zPeT&*pm%2{)XiOOJQ|+E=w5wdNdI5=sK@)|XNz-HX-a zA_4-)hE-Q40i#l}g}F(G*8e82VAXYR^KeV_D9+O5cGfc~I19>m(XjOxm9q3WDFYD4 zv*Gj4B*TmJ9C9gEkg$I#9;Z#SO?zdP9AAn0BVU93XQ#hb+S?ArbLc!Z%1 z-D&W^qL?w@T%Fhbwz6{J<$Jmc2X7==A(XJz(>X{dhG^D`#?L>ii@`9p(%%rw;$t1AC1`U z{H;I2;w^zgdG3{SG(!D>-+BCS&-3umngHe=+Ni#`a!M9+HaZ7X{VRE6NXJ9&rP3}$ zbtihg=Zzq4oqNyQ-befjwBWgh*EG*C(9RD+biD>q6(@Q~A{H+BFX}n0LRW)X>kf zX_{tM;*++8(y7aBnWA+w_uA}>>P6>58+Y{3hh2J{b!wubCJ5OS-QO>on^x3FeD1-Z z8#m0(pM#&6Z7?M3K5~7_OBX47t|f2y<6cJMo@QT*#`3Bay2zW)xA=qd-Ia)7AN3bNHDFD!|2g@2d#0>nNGiY zHB3(8*0kDk+;0k(TEXAFtHyDuC8FIqn%&f)wn^TVEH26AR0KDw;rd&1;?an>w*QH< zzwt><)j{mbpI3k2BKBeTfAoUVxu(mXk5^`J6)k&qL^6XDX9+{Z)%6`#o>zFMFgQrq zW#l)IwmBO{{rE@A8<(|H{5cymLsJzHw|^nsb~72>_GJ+nu85Wu-SEbQw@G@u{r~p@ zi`vjk%a{N≶M9Zv|5`G?V+t`dH%WEMXqpk;_3Hk#c)5bow}^>GHdOg=@5pYx=49b13Ly zrJynzDRebu8rnRSaDCBo@|9g53n?-Toj%;)7@=UY|Noy}Ma{<^CTZW`I9PGat{6t2 zaDw4J3oUfqrNwG{*q4kXjQO#)d%E?I`6unq0@g?R`EPa45Y|xpiRX?D^@^QqfPjrp z`1!tA9;wNGs_VH%HtUBo8*1_;v~g)avtfdTTKYQ+w|_T@R1|@!z4MWbhw@XQ#{r8n zFBZwGL~*S&Epds65DN=S9LDmC%OD``N?Ja3mq@-KYHmwkg<5GJXlu`C1U~hF;}GUt zEV_svDDRB0pxY;h0iVB6!+t9MoPjoUN2{tnNvmp71!_;3|F6OefCDX3EgVYSHa&Y~ zU}M_~aPglu-?Am;VW9ar%IVbGkq@`{Tq5wAt!;iU^4x0?c1udN{X&%ux4V&|KJ4W8 z^-G%6%-Mk6r|NGgPt?6Gz6|&!zRxhaV4(F`GZ8rK4j__!PX?Q7^;*CQ1mh5%DvR6C z?e!IxGMT8~xw^^9NiPO6=pQ@SRODn~Fsq8jD|VmScW#=CFmzog>dWa9ISWyD>bX6* zxnZ#2TwN0;af`pj^X|5h-UR+^#wtJ!B@tKiK!pf5VjK5!fZLt zzX!?qwZ#;g@k9fI%Zc#zkaZg@VAO57d!5O3Ue&0UhOx8iALeb!OI zRvYFgz{G^aMMxIXA9Q8%Ks(ns{Ng8LwsDMRXV9_!HbeTZg#-D*^A&&kWc>8s7s|rI z=ynSoO>Z_is~m>%blt}gX#It_>e2=$ntV|mjceXzzfgYM^uWLH4|qn@T1Vx$y+(mB zr_oTl_0pJ8ZR}4L)pN;gX0*r7gHMr7oprWF%6?*dA?da-61JtJNHWwP^#36TIN#Kl zHX65H`VqI9GB?_ee-?H!Yb?FpyYadu!+UaImNKBnpF_sZf6ay)B7}^R1hodXNP?eK z4UhCNG=>^Ra(w)t-DekO(;#W^pCZ0-I00f84I3?O?7O zB%x)J$UIIl{lRxlFHDbkes}4Xh27aeZ@lufb-e26$_f|NxU&YtVUJG#2x>BxrM_VU z1LCx!(BL?8gDF{Vq$J)xNc>yL)$=)7I*g|%)(&0-;j8CsUSvGa_^S6DVMpH zb8v0T>G{!ohHICj*>1fftNM+zZtws0KCZLv(IK>C_QT1EH@fD(8$2iPv~!>Y4cV!P zSZxtr!>L43HPE+4<>ftO;-5eywKK$aj&E__@NZF$Ub2O545&GURXRcHZf+@4Q*sxY zWp376swn?@b?YH5czxG41HZOjt)n5ymnfWVn}~l0v!W9f09Qb$zl@4>j!2BD$PIWy zG%tHq1co&qIS~ddbB3;u@9Dn~xdsYnr5s8^`W*i3m@VDEnOB&F?o#)Rs5v1eUah$w>#9 z(>0sd(bH1S1!0KMO_Fm9-(30B*tAvEi!I8bj@kN6Z10~`=5`eh*Oh@TSgc~GARo=W z<9@k$y19A1xpq3jbDRG4LD38!4XW&4?>FThiiaoIiVSTRo?|YzPPb-muv!>myEIOW zt#-bt4&U((ZdDWewL`sy(~|m2`BN@QF+Q{|G9ZCrd6ta$KC*E*TQ_G)_3NXw*H*CY zbEL^@$wN>XGaLNe4bu&0*GJMUg5zS=CN)LQyqHShcMq=mcw_4h9+%ozHC=V~`$%?N z`DWW#&TppMgR}4%!g!nK9J8-_y1Mk=F&oUrC@E68`W@5@I(HpHbiw(p1ld9~Keub^ z`qq-^lE(GMES@?`16xSt_yAA?b$A0;omip zd;&xiYAZP+=TTDO@|)ZHN*1!_{_wQ@i@zy5-UG{`HCbB7dqN5AgmRL@R~5 z-KAmZe7J3gow^U;9@e_R+QBj&j{H=={FCM3!&hwmwI9#NDh>OjBmRdo%0A!wRKaXY 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

    diff --git a/docs/Heroku.md b/docs/Heroku.md index 0e75c49b0..5c90e2ab4 100644 --- a/docs/Heroku.md +++ b/docs/Heroku.md @@ -1,8 +1,8 @@ ```sh -git clone git://github.com/swanson/stringer.git +git clone git@github.com:stringer-rss/stringer.git cd stringer heroku create -git push heroku master +git push heroku main heroku config:set APP_URL=`heroku apps:info --shell | grep web_url | cut -d= -f2` heroku config:set SECRET_TOKEN=`openssl rand -hex 20` @@ -26,7 +26,7 @@ From the app's directory: ```sh git pull -git push heroku master +git push heroku main heroku run rake db:migrate heroku restart ``` diff --git a/docs/OpenShift.md b/docs/OpenShift.md index be9d7dbfe..3299cd066 100644 --- a/docs/OpenShift.md +++ b/docs/OpenShift.md @@ -14,8 +14,8 @@ Deploying into OpenShift ```sh cd feeds - git remote add upstream git://github.com/swanson/stringer.git - git pull -s recursive -X theirs upstream master + git remote add upstream git@github.com:stringer-rss/stringer.git + git pull -s recursive -X theirs upstream main ``` 3. To enable migrations for the application, a new action_hook is required. Add the file, .openshift/action_hooks/deploy, with the below 3 lines into it. diff --git a/docs/VPS.md b/docs/VPS.md index 4bb635de7..9c05452d2 100644 --- a/docs/VPS.md +++ b/docs/VPS.md @@ -79,7 +79,7 @@ Install Stringer and set it up Grab Stringer from github - git clone https://github.com/swanson/stringer.git + git clone git@github.com:stringer-rss/stringer.git cd stringer Use bundler to grab and build Stringer's dependencies diff --git a/docs/docker.md b/docs/docker.md index 72f252a32..442553f30 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -40,7 +40,7 @@ docker run --detach \ -e FETCH_FEEDS_CRON="*/5 * * * *" \ # optional -e CLEANUP_CRON="0 0 * * *" \ # optional -p 127.0.0.1:8080:8080 \ - mdswanson/stringer + stringer-rss/stringer ``` That's it! You now have a fully working Stringer instance up and running! diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index aacd5840d..60ab88c0c 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -44,7 +44,7 @@ page = last_response.body expect(page).to have_tag("a", with: { href: "/feeds/export" }) expect(page).to have_tag("a", with: { href: "/logout" }) - expect(page).to have_tag("a", with: { href: "https://github.com/swanson/stringer" }) + expect(page).to have_tag("a", with: { href: "https://github.com/stringer-rss/stringer" }) end it "displays a zen-like message when there are no unread stories" do diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index fedac02c1..463b877f2 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -369,16 +369,16 @@ describe ".extract_url" do it "returns the url" do feed = double(url: "http://github.com") - entry = double(url: "https://github.com/swanson/stringer") + entry = double(url: "https://github.com/stringer-rss/stringer") - expect(StoryRepository.extract_url(entry, feed)).to eq "https://github.com/swanson/stringer" + expect(StoryRepository.extract_url(entry, feed)).to eq "https://github.com/stringer-rss/stringer" end it "returns the enclosure_url when the url is nil" do feed = double(url: "http://github.com") - entry = double(url: nil, enclosure_url: "https://github.com/swanson/stringer") + entry = double(url: nil, enclosure_url: "https://github.com/stringer-rss/stringer") - expect(StoryRepository.extract_url(entry, feed)).to eq "https://github.com/swanson/stringer" + expect(StoryRepository.extract_url(entry, feed)).to eq "https://github.com/stringer-rss/stringer" end it "does not crash if url is nil but enclosure_url does not exist" do From f612c0f3853987e724fcc12caf673c0ced793793 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Fri, 14 Oct 2022 12:49:32 -0700 Subject: [PATCH 0357/1107] update deploy button link (#651) For some reason it doesn't work without the `template` param for me. Maybe I'm using a privacy extension that strips out the `referer`. Explicitly passing the `template` will provide a more reliable experience, even if it's a little less flexible, as forks will no longer be able to be deployed as easily. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7569ef96..5f5b44196 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ But it does have keyboard shortcuts and was made with love! Stringer is a Ruby app based on Sinatra, ActiveRecord, PostgreSQL, Backbone.js and DelayedJob. -[![Deploy to Heroku](https://cdn.herokuapp.com/deploy/button.svg)](https://heroku.com/deploy) +[![Deploy to Heroku](https://cdn.herokuapp.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/stringer-rss/stringer) Stringer will run just fine on the Heroku free plan. From 5ef0426e39935dd83fdced7dca9cf5b46ebc5f52 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Oct 2022 21:47:14 -0700 Subject: [PATCH 0358/1107] Bump nokogiri from 1.13.8 to 1.13.9 (#654) Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.13.8 to 1.13.9. - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.13.8...v1.13.9) --- updated-dependencies: - dependency-name: nokogiri dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5fe9e54fb..e1138ce1a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -71,7 +71,7 @@ GEM mustermann (3.0.0) ruby2_keywords (~> 0.0.1) nio4r (2.5.8) - nokogiri (1.13.8) + nokogiri (1.13.9) mini_portile2 (~> 2.8.0) racc (~> 1.4) parallel (1.22.1) From 3551e87ed7d1f861f5786f7160cd5d70f3d5bf33 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 23 Oct 2022 11:51:48 -0700 Subject: [PATCH 0359/1107] Set a logger inside FetchFeed (#655) Logging feed errors should be helpful for debugging. --- app/tasks/fetch_feed.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tasks/fetch_feed.rb b/app/tasks/fetch_feed.rb index e52862ce9..c683823e3 100644 --- a/app/tasks/fetch_feed.rb +++ b/app/tasks/fetch_feed.rb @@ -6,7 +6,7 @@ require_relative "../commands/feeds/find_new_stories" class FetchFeed - def initialize(feed, parser: Feedjira, client: HTTParty, logger: nil) + def initialize(feed, parser: Feedjira, client: HTTParty, logger: Logger.new($stdout)) @feed = feed @parser = parser @client = client From bab8f165a7d08320981e060c1ee4eeef47ff9ccd Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Mon, 24 Oct 2022 10:56:06 -0700 Subject: [PATCH 0360/1107] Deps: lock down puma version (#657) The latest version of Puma causes some breakage in Capybara, so we'll lock it down until Capybara gets around to releasing an update. --- .rubocop_todo.yml | 11 ++++++----- Gemfile | 2 +- Gemfile.lock | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 48f3f4ee5..a327a4052 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 400` -# on 2022-09-19 17:13:31 UTC using RuboCop version 1.36.0. +# on 2022-10-24 17:51:19 UTC using RuboCop version 1.36.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -13,12 +13,13 @@ Bundler/GemComment: Exclude: - 'Gemfile' -# Offense count: 40 -# Configuration parameters: Include, AllowedGems. +# Offense count: 39 +# Configuration parameters: EnforcedStyle, Include, AllowedGems. # SupportedStyles: required, forbidden # Include: **/*.gemfile, **/Gemfile, **/gems.rb Bundler/GemVersion: - EnforcedStyle: forbidden + Exclude: + - 'Gemfile' # Offense count: 9 # This cop supports safe autocorrection (--autocorrect). @@ -102,7 +103,7 @@ Lint/AmbiguousOperatorPrecedence: - 'spec/factories/feed_factory.rb' - 'spec/factories/group_factory.rb' -# Offense count: 765 +# Offense count: 766 # Configuration parameters: Only, Ignore. Lint/ConstantResolution: Exclude: diff --git a/Gemfile b/Gemfile index 5e9b686a8..575f0885a 100644 --- a/Gemfile +++ b/Gemfile @@ -33,7 +33,7 @@ gem "i18n" gem "loofah" gem "nokogiri" gem "pg" -gem "puma" +gem "puma", "~> 5.6" gem "rack-protection" gem "racksh" gem "rack-ssl" diff --git a/Gemfile.lock b/Gemfile.lock index e1138ce1a..17cfb246d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -215,7 +215,7 @@ DEPENDENCIES nokogiri pg pry-byebug - puma + puma (~> 5.6) rack-protection rack-ssl rack-test From 59d5e8e682ea7278f3f6848159b94109bbb5bfed Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 11:04:15 -0700 Subject: [PATCH 0361/1107] Update all Bundler dependencies (2022-10-24) (#656) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- Gemfile.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 17cfb246d..56b5f621b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -45,7 +45,7 @@ GEM i18n (>= 1.8.11, < 2) feedbag (1.0.0) nokogiri (~> 1.8, >= 1.8.2) - feedjira (3.2.1) + feedjira (3.2.2) loofah (>= 2.3.1) sax-machine (>= 1.0) ffi (1.15.5) @@ -77,7 +77,7 @@ GEM parallel (1.22.1) parser (3.1.2.1) ast (~> 2.4.1) - pg (1.4.3) + pg (1.4.4) pry (0.14.1) coderay (~> 1.1) method_source (~> 1.0) @@ -121,25 +121,25 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.11.0) rspec-support (3.11.1) - rubocop (1.36.0) + rubocop (1.37.1) json (~> 2.3) parallel (~> 1.10) parser (>= 3.1.2.1) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.20.1, < 2.0) + rubocop-ast (>= 1.23.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.21.0) + rubocop-ast (1.23.0) parser (>= 3.1.1.0) - rubocop-rails (2.16.1) + rubocop-rails (2.17.0) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) rubocop-rake (0.6.0) rubocop (~> 1.0) - rubocop-rspec (2.13.2) + rubocop-rspec (2.14.1) rubocop (~> 1.33) ruby-progressbar (1.11.0) ruby2_keywords (0.0.5) @@ -162,7 +162,7 @@ GEM rack (~> 2.2, >= 2.2.4) rack-protection (= 3.0.2) tilt (~> 2.0) - sinatra-activerecord (2.0.25) + sinatra-activerecord (2.0.26) activerecord (>= 4.1) sinatra (>= 1.0) sinatra-contrib (3.0.2) From 591c515836916378186e0466659811254b8e99fb Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 09:40:30 -0700 Subject: [PATCH 0362/1107] Update all Bundler dependencies (2022-10-31) (#658) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- Gemfile.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 56b5f621b..212b72828 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -105,22 +105,22 @@ GEM ffi (~> 1.0) regexp_parser (2.6.0) rexml (3.2.5) - rspec (3.11.0) - rspec-core (~> 3.11.0) - rspec-expectations (~> 3.11.0) - rspec-mocks (~> 3.11.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.1) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.0) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) + rspec-support (~> 3.12.0) rspec-html-matchers (0.10.0) nokogiri (~> 1) rspec (>= 3.0.0.a) - rspec-mocks (3.11.1) + rspec-mocks (3.12.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-support (3.11.1) + rspec-support (~> 3.12.0) + rspec-support (3.12.0) rubocop (1.37.1) json (~> 2.3) parallel (~> 1.10) @@ -133,13 +133,13 @@ GEM unicode-display_width (>= 1.4.0, < 3.0) rubocop-ast (1.23.0) parser (>= 3.1.1.0) - rubocop-rails (2.17.0) + rubocop-rails (2.17.2) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) rubocop-rake (0.6.0) rubocop (~> 1.0) - rubocop-rspec (2.14.1) + rubocop-rspec (2.14.2) rubocop (~> 1.33) ruby-progressbar (1.11.0) ruby2_keywords (0.0.5) From 7f647da491d603da70c4f7e3d12531e5393a38c4 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Mon, 7 Nov 2022 09:18:31 -0800 Subject: [PATCH 0363/1107] Update all Bundler dependencies (2022-11-07) (#659) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 212b72828..b1bd008c1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,7 +16,7 @@ GEM ast (2.4.2) bcrypt (3.1.18) byebug (11.1.3) - capybara (3.37.1) + capybara (3.38.0) addressable matrix mini_mime (>= 0.1.3) @@ -41,7 +41,7 @@ GEM diff-lcs (1.5.0) docile (1.4.0) execjs (2.8.1) - faker (2.23.0) + faker (3.0.0) i18n (>= 1.8.11, < 2) feedbag (1.0.0) nokogiri (~> 1.8, >= 1.8.2) @@ -121,7 +121,7 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-support (3.12.0) - rubocop (1.37.1) + rubocop (1.38.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.1.2.1) @@ -139,7 +139,7 @@ GEM rubocop (>= 1.33.0, < 2.0) rubocop-rake (0.6.0) rubocop (~> 1.0) - rubocop-rspec (2.14.2) + rubocop-rspec (2.15.0) rubocop (~> 1.33) ruby-progressbar (1.11.0) ruby2_keywords (0.0.5) From 8433d0f8dabd77a3dd775a0ca89d3ba7ed71d7d4 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 10:34:18 -0800 Subject: [PATCH 0364/1107] Update all Bundler dependencies (2022-11-14) (#660) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- Gemfile.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b1bd008c1..8e5998c2c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -89,7 +89,7 @@ GEM nio4r (~> 2.0) racc (1.6.0) rack (2.2.4) - rack-protection (3.0.2) + rack-protection (3.0.3) rack rack-ssl (1.4.1) rack @@ -121,7 +121,7 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-support (3.12.0) - rubocop (1.38.0) + rubocop (1.39.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.1.2.1) @@ -157,19 +157,19 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - sinatra (3.0.2) + sinatra (3.0.3) mustermann (~> 3.0) rack (~> 2.2, >= 2.2.4) - rack-protection (= 3.0.2) + rack-protection (= 3.0.3) tilt (~> 2.0) sinatra-activerecord (2.0.26) activerecord (>= 4.1) sinatra (>= 1.0) - sinatra-contrib (3.0.2) + sinatra-contrib (3.0.3) multi_json mustermann (~> 3.0) - rack-protection (= 3.0.2) - sinatra (= 3.0.2) + rack-protection (= 3.0.3) + sinatra (= 3.0.3) tilt (~> 2.0) sinatra-flash (0.3.0) sinatra (>= 1.0.0) From 69b153c1a48461dc8f1a8dacd92e7c0ede4ab18b Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Mon, 21 Nov 2022 09:01:58 -0800 Subject: [PATCH 0365/1107] Update all Bundler dependencies (2022-11-21) (#661) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8e5998c2c..2299b778b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -77,7 +77,7 @@ GEM parallel (1.22.1) parser (3.1.2.1) ast (~> 2.4.1) - pg (1.4.4) + pg (1.4.5) pry (0.14.1) coderay (~> 1.1) method_source (~> 1.0) @@ -103,7 +103,7 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - regexp_parser (2.6.0) + regexp_parser (2.6.1) rexml (3.2.5) rspec (3.12.0) rspec-core (~> 3.12.0) @@ -133,7 +133,7 @@ GEM unicode-display_width (>= 1.4.0, < 3.0) rubocop-ast (1.23.0) parser (>= 3.1.1.0) - rubocop-rails (2.17.2) + rubocop-rails (2.17.3) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) @@ -185,7 +185,7 @@ GEM thread (0.2.2) tilt (2.0.11) timecop (0.9.5) - tins (1.31.1) + tins (1.32.0) sync tzinfo (2.0.5) concurrent-ruby (~> 1.0) From a9a32516e6ba0bacf3f1940e3c16b06b1e3b73a9 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Sun, 27 Nov 2022 22:37:17 -0800 Subject: [PATCH 0366/1107] Update all Bundler dependencies (2022-11-28) (#663) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- Gemfile.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2299b778b..ba5745e84 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -75,7 +75,7 @@ GEM mini_portile2 (~> 2.8.0) racc (~> 1.4) parallel (1.22.1) - parser (3.1.2.1) + parser (3.1.3.0) ast (~> 2.4.1) pg (1.4.5) pry (0.14.1) @@ -89,7 +89,7 @@ GEM nio4r (~> 2.0) racc (1.6.0) rack (2.2.4) - rack-protection (3.0.3) + rack-protection (3.0.4) rack rack-ssl (1.4.1) rack @@ -157,19 +157,19 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - sinatra (3.0.3) + sinatra (3.0.4) mustermann (~> 3.0) rack (~> 2.2, >= 2.2.4) - rack-protection (= 3.0.3) + rack-protection (= 3.0.4) tilt (~> 2.0) sinatra-activerecord (2.0.26) activerecord (>= 4.1) sinatra (>= 1.0) - sinatra-contrib (3.0.3) + sinatra-contrib (3.0.4) multi_json mustermann (~> 3.0) - rack-protection (= 3.0.3) - sinatra (= 3.0.3) + rack-protection (= 3.0.4) + sinatra (= 3.0.4) tilt (~> 2.0) sinatra-flash (0.3.0) sinatra (>= 1.0.0) @@ -185,7 +185,7 @@ GEM thread (0.2.2) tilt (2.0.11) timecop (0.9.5) - tins (1.32.0) + tins (1.32.1) sync tzinfo (2.0.5) concurrent-ruby (~> 1.0) From 62c8a0f5f5c63b359e42a1cdbade5de1b36cd74a Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Tue, 29 Nov 2022 16:23:55 -0800 Subject: [PATCH 0367/1107] Update puma to version 6.0.0 (#664) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- Gemfile | 2 +- Gemfile.lock | 4 ++-- config/puma.rb | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 575f0885a..6174e4a96 100644 --- a/Gemfile +++ b/Gemfile @@ -33,7 +33,7 @@ gem "i18n" gem "loofah" gem "nokogiri" gem "pg" -gem "puma", "~> 5.6" +gem "puma", "~> 6.0" gem "rack-protection" gem "racksh" gem "rack-ssl" diff --git a/Gemfile.lock b/Gemfile.lock index ba5745e84..68a3f0c18 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -85,7 +85,7 @@ GEM byebug (~> 11.0) pry (>= 0.13, < 0.15) public_suffix (5.0.0) - puma (5.6.5) + puma (6.0.0) nio4r (~> 2.0) racc (1.6.0) rack (2.2.4) @@ -215,7 +215,7 @@ DEPENDENCIES nokogiri pg pry-byebug - puma (~> 5.6) + puma (~> 6.0) rack-protection rack-ssl rack-test diff --git a/config/puma.rb b/config/puma.rb index 968c37cf0..0e079d63c 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -2,7 +2,6 @@ threads_count = Integer(ENV.fetch("MAX_THREADS", 2)) threads threads_count, threads_count -rackup DefaultRackup port ENV.fetch("PORT", 3000) environment ENV.fetch("RACK_ENV", "development") From bd36c54d7d5310e38151f21204713013c5e42f17 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Tue, 29 Nov 2022 16:34:24 -0800 Subject: [PATCH 0368/1107] Update Ruby to version 3.1.3 (#662) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- .circleci/config.yml | 8 ++++---- .ruby-version | 2 +- Dockerfile | 2 +- Gemfile.lock | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8c5336ecb..6c8e2edd9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ jobs: build: parallelism: 1 docker: - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.1.3-browsers environment: BUNDLE_JOBS: 3 BUNDLE_RETRY: 3 @@ -39,8 +39,8 @@ jobs: # https://circleci.com/docs/2.0/caching/ - restore_cache: keys: - - bundle-{{ checksum "Gemfile.lock" }} - - bundle- + - bundle-v1-{{ checksum "Gemfile.lock" }} + - bundle-v1- - run: # Install Ruby dependencies name: Bundle Install @@ -51,7 +51,7 @@ jobs: # command: bundle exec bundle audit - save_cache: - key: bundle-{{ checksum "Gemfile.lock" }} + key: bundle-v1-{{ checksum "Gemfile.lock" }} paths: - vendor/bundle diff --git a/.ruby-version b/.ruby-version index ef538c281..ff365e06b 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.1.2 +3.1.3 diff --git a/Dockerfile b/Dockerfile index e5952ba18..307350dfb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.1.2 +FROM ruby:3.1.3 ENV RACK_ENV=production ENV PORT=8080 diff --git a/Gemfile.lock b/Gemfile.lock index 68a3f0c18..c47468fa1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -242,7 +242,7 @@ DEPENDENCIES will_paginate RUBY VERSION - ruby 3.1.2 + ruby 3.1.3 BUNDLED WITH 2.2.33 From 66b7d730b7454419dc5082b73fe8ab4387cc675d Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 15:58:10 -0800 Subject: [PATCH 0369/1107] Update all Bundler dependencies (2022-12-05) (#665) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c47468fa1..d3a14d15e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -87,7 +87,7 @@ GEM public_suffix (5.0.0) puma (6.0.0) nio4r (~> 2.0) - racc (1.6.0) + racc (1.6.1) rack (2.2.4) rack-protection (3.0.4) rack @@ -131,7 +131,7 @@ GEM rubocop-ast (>= 1.23.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.23.0) + rubocop-ast (1.24.0) parser (>= 3.1.1.0) rubocop-rails (2.17.3) activesupport (>= 4.2.0) @@ -184,7 +184,7 @@ GEM thor (1.2.1) thread (0.2.2) tilt (2.0.11) - timecop (0.9.5) + timecop (0.9.6) tins (1.32.1) sync tzinfo (2.0.5) From 4e654c6d764f2fd19866c65dfe350ef09bb44ebf Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 6 Dec 2022 21:04:23 -0800 Subject: [PATCH 0370/1107] Remove unused files (#669) I'm not seeing any usage of these files in the codebase. It appears they were used before when there was a `/read` page, but that was renamed to `/archive`. --- app/views/partials/_story.erb | 46 ----------------------------------- app/views/read.erb | 26 -------------------- 2 files changed, 72 deletions(-) delete mode 100644 app/views/partials/_story.erb delete mode 100644 app/views/read.erb diff --git a/app/views/partials/_story.erb b/app/views/partials/_story.erb deleted file mode 100644 index 19926f492..000000000 --- a/app/views/partials/_story.erb +++ /dev/null @@ -1,46 +0,0 @@ -<% if story.is_read %> -
  • -<% else %> -
  • -<% end %> -
    -
    -

    - <%= story.source %> -

    -
    -
    -

    - - <%= story.headline %> - - - — <%= story.lead %> - -

    -
    -
    - - - -
  • \ No newline at end of file diff --git a/app/views/read.erb b/app/views/read.erb deleted file mode 100644 index b37ad9663..000000000 --- a/app/views/read.erb +++ /dev/null @@ -1,26 +0,0 @@ -
    - <%= render_partial :feed_action_bar %> -
    - -<% unless @read_stories.empty? %> -
    -
      - <% @read_stories.each do |story| %> - <%= render_partial :story, { story: story } %> - <% end %> -
    -
    - -<% else %> -
    -

    Sorry, you haven't read any stories yet!

    -
    -<% end %> \ No newline at end of file From 5e3bef97d6f15881c61a0fc333b941b8dde310ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Dec 2022 10:31:34 -0800 Subject: [PATCH 0371/1107] Bump nokogiri from 1.13.9 to 1.13.10 (#670) Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.13.9 to 1.13.10. - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.13.9...v1.13.10) --- updated-dependencies: - dependency-name: nokogiri dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d3a14d15e..ea3f69a40 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -71,7 +71,7 @@ GEM mustermann (3.0.0) ruby2_keywords (~> 0.0.1) nio4r (2.5.8) - nokogiri (1.13.9) + nokogiri (1.13.10) mini_portile2 (~> 2.8.0) racc (~> 1.4) parallel (1.22.1) From beb4245b3fe48667592c7a90a41fa956bf457d7d Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 10 Dec 2022 15:16:26 -0800 Subject: [PATCH 0372/1107] Add download link when story has enclosure (#667) Many podcast and other media feeds have both a `url` and an `enclosure_url`. For example, [the Welcome to Nightvale feed][wn]. In these cases, there's a good chance the user might want to get straight to the enclosure rather than being directed to another site to listen to the track. This allows us to directly download the enclosure rather than having to hunt it down somewhere else. [wn]: http://feeds.nightvalepresents.com/welcometonightvalepodcast --- .rubocop_todo.yml | 1 + app/assets/stylesheets/application.css | 2 +- app/repositories/story_repository.rb | 2 ++ app/views/js/templates/_story.js.erb | 5 +++++ ...1206231914_add_enclosure_url_to_stories.rb | 7 ++++++ db/schema.rb | 4 ++-- spec/javascript/spec/views/story_view_spec.js | 17 ++++++++++++++ spec/models/story_spec.rb | 1 + spec/repositories/story_repository_spec.rb | 22 +++++++++++++++++++ 9 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20221206231914_add_enclosure_url_to_stories.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index a327a4052..d80bd135b 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -184,6 +184,7 @@ Lint/ConstantResolution: - 'db/migrate/20140413100725_add_groups_table_and_foreign_keys_to_feeds.rb' - 'db/migrate/20140421224454_fix_invalid_unicode.rb' - 'db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb' + - 'db/migrate/20221206231914_add_enclosure_url_to_stories.rb' - 'fever_api.rb' - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/commands/feeds/export_to_opml_spec.rb' diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index acf41f639..6b8864c83 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -207,7 +207,7 @@ li.story.open .story-preview { margin-left: 20px; } -.story-keep-unread, .story-starred { +.story-keep-unread, .story-starred, .story-enclosure { display: inline-block; cursor: pointer; -webkit-touch-callout: none; diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index 73a65d904..75fbdd1b9 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -7,9 +7,11 @@ class StoryRepository extend UrlHelpers def self.add(entry, feed) + enclosure_url = entry.enclosure_url if entry.respond_to?(:enclosure_url) Story.create(feed: feed, title: extract_title(entry), permalink: extract_url(entry, feed), + enclosure_url: enclosure_url, body: extract_content(entry), is_read: false, is_starred: false, diff --git a/app/views/js/templates/_story.js.erb b/app/views/js/templates/_story.js.erb index dae33201a..874520322 100644 --- a/app/views/js/templates/_story.js.erb +++ b/app/views/js/templates/_story.js.erb @@ -38,6 +38,11 @@
    + {{ if (enclosure_url) { }} + + + + {{ } }} diff --git a/db/migrate/20221206231914_add_enclosure_url_to_stories.rb b/db/migrate/20221206231914_add_enclosure_url_to_stories.rb new file mode 100644 index 000000000..1c20b317d --- /dev/null +++ b/db/migrate/20221206231914_add_enclosure_url_to_stories.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddEnclosureUrlToStories < ActiveRecord::Migration[4.2] + def change + add_column(:stories, :enclosure_url, :string) + end +end diff --git a/db/schema.rb b/db/schema.rb index f4d5a1075..d55f58167 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,8 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2014_11_02_103617) do - +ActiveRecord::Schema[7.0].define(version: 2022_12_06_231914) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -59,6 +58,7 @@ t.boolean "keep_unread", default: false t.boolean "is_starred", default: false t.text "entry_id" + t.string "enclosure_url" t.index ["entry_id", "feed_id"], name: "index_stories_on_entry_id_and_feed_id", unique: true end diff --git a/spec/javascript/spec/views/story_view_spec.js b/spec/javascript/spec/views/story_view_spec.js index c7a556c97..737ac7f5e 100644 --- a/spec/javascript/spec/views/story_view_spec.js +++ b/spec/javascript/spec/views/story_view_spec.js @@ -9,6 +9,7 @@ describe("Storyiew", function(){ before(function() { this.story = new Story({ source: "TechKrunch", + enclosure_url: null, headline: "Every startups acquired by Yahoo!", lead: "This is the lead.", title: "Every startups acquired by Yahoo! NOT!!", @@ -34,6 +35,10 @@ describe("Storyiew", function(){ el.find(tagName).should.have.length(count); }; + var assertNoTagExists = function(el, tagName) { + el.find(tagName).should.have.length(0); + }; + var assertPropertyRendered = function(el, model, propName) { el.html().should.have.string(model.get(propName)); }; @@ -104,6 +109,18 @@ describe("Storyiew", function(){ assertTagExists(this.view.$el, ".story-starred .icon-star", 2); }); + it("should not render enclosure link when not present", function(){ + assertNoTagExists(this.view.$el, ".story-enclosure"); + }); + + it("should render enclosure link when present", function(){ + this.story.set("enclosure_url", "http://example.com/enclosure"); + this.view.render(); + + assertTagExists(this.view.$el, ".story-enclosure"); + assertPropertyRendered(this.view.$el, this.story, "enclosure_url"); + }); + describe("Handling click on story", function(){ beforeEach(function() { this.toggle_stub = sinon.stub(this.story, "toggle"); diff --git a/spec/models/story_spec.rb b/spec/models/story_spec.rb index b695f5ff6..e0674b4cc 100644 --- a/spec/models/story_spec.rb +++ b/spec/models/story_spec.rb @@ -85,6 +85,7 @@ expect(story.as_json).to eq({ body: "story body", created_at: created_at.utc.as_json, + enclosure_url: nil, entry_id: "5", feed_id: feed.id, headline: "the story title", diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 463b877f2..19ea42bb0 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -34,6 +34,28 @@ StoryRepository.add(entry, feed) end + + it "sets the enclosure url when present" do + entry = instance_double(Feedjira::Parser::ITunesRSSItem, + enclosure_url: "http://example.com/audio.mp3", + title: "", + summary: "", + content: "").as_null_object + allow(StoryRepository).to receive(:normalize_url) + + expect(Story).to receive(:create).with(hash_including(enclosure_url: "http://example.com/audio.mp3")) + + StoryRepository.add(entry, feed) + end + + it "does not set the enclosure url when not present" do + entry = instance_double(Feedjira::Parser::RSSEntry, title: "", summary: "", content: "").as_null_object + allow(StoryRepository).to receive(:normalize_url) + + expect(Story).to receive(:create).with(hash_including(enclosure_url: nil)) + + StoryRepository.add(entry, feed) + end end describe ".fetch" do From 68e9c3d17b1ceea39b8f65c888bc4c43c463aa4a Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 10:12:17 -0800 Subject: [PATCH 0373/1107] Update all Bundler dependencies (2022-12-12) (#673) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- .rubocop.yml | 1 + Gemfile.lock | 8 ++++---- spec/controllers/feeds_controller_spec.rb | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 80584e7ad..a616fda72 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -52,3 +52,4 @@ Style/StringLiterals: ################################################################################ Rails/SchemaComment: { Enabled: false } +Style/RequireOrder: { Enabled: false } diff --git a/Gemfile.lock b/Gemfile.lock index ea3f69a40..ce444c04f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,7 +54,7 @@ GEM multi_xml (>= 0.5.2) i18n (1.12.0) concurrent-ruby (~> 1.0) - json (2.6.2) + json (2.6.3) loofah (2.19.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -84,7 +84,7 @@ GEM pry-byebug (3.10.1) byebug (~> 11.0) pry (>= 0.13, < 0.15) - public_suffix (5.0.0) + public_suffix (5.0.1) puma (6.0.0) nio4r (~> 2.0) racc (1.6.1) @@ -117,11 +117,11 @@ GEM rspec-html-matchers (0.10.0) nokogiri (~> 1) rspec (>= 3.0.0.a) - rspec-mocks (3.12.0) + rspec-mocks (3.12.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-support (3.12.0) - rubocop (1.39.0) + rubocop (1.40.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.1.2.1) diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index 90c8c3d3d..3917ba408 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -87,7 +87,7 @@ expect(AddNewFeed).to receive(:add).with(feed_url).and_return(valid_feed) expect(FetchFeeds).to receive(:enqueue).with([valid_feed]) - post "/feeds", feed_url: feed_url + post("/feeds", feed_url:) expect(last_response.status).to be 302 expect(URI.parse(last_response.location).path).to eq "/" @@ -100,7 +100,7 @@ it "adds the feed and queues it to be fetched" do expect(AddNewFeed).to receive(:add).with(feed_url).and_return(false) - post "/feeds", feed_url: feed_url + post("/feeds", feed_url:) page = last_response.body expect(page).to have_tag(".error") @@ -114,7 +114,7 @@ it "adds the feed and queues it to be fetched" do expect(AddNewFeed).to receive(:add).with(feed_url).and_return(invalid_feed) - post "/feeds", feed_url: feed_url + post("/feeds", feed_url:) page = last_response.body expect(page).to have_tag(".error") From b8da572be2286ece12a67af64352b157510e895c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Dec 2022 15:24:56 -0800 Subject: [PATCH 0374/1107] Bump loofah from 2.19.0 to 2.19.1 (#674) Bumps [loofah](https://github.com/flavorjones/loofah) from 2.19.0 to 2.19.1. - [Release notes](https://github.com/flavorjones/loofah/releases) - [Changelog](https://github.com/flavorjones/loofah/blob/main/CHANGELOG.md) - [Commits](https://github.com/flavorjones/loofah/compare/v2.19.0...v2.19.1) --- updated-dependencies: - dependency-name: loofah dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index ce444c04f..56f3d615f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -55,7 +55,7 @@ GEM i18n (1.12.0) concurrent-ruby (~> 1.0) json (2.6.3) - loofah (2.19.0) + loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) matrix (0.4.2) From 4a63bc56498a35a6d17a11a5ee8e5433fb1a9c70 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Wed, 14 Dec 2022 20:14:27 -0800 Subject: [PATCH 0375/1107] Deps: add a `.tool-versions` file (#676) This file allows us to use tools like [`asdf`][asdf] to manage a wide variety of application dependencies. [asdf]: https://github.com/asdf-vm/asdf --- .tool-versions | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..a19bf3ddd --- /dev/null +++ b/.tool-versions @@ -0,0 +1,3 @@ +ruby 3.1.3 +bundler 2.2.33 +postgres 14.6 From f251cb48c3cc30d230c07c616d93206d9f4445d0 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 15 Dec 2022 13:08:38 -0800 Subject: [PATCH 0376/1107] Deps: update bundler version (#677) This updates our bundler version to match the latest version [currently supported by Heroku][he]. [he]: https://devcenter.heroku.com/articles/ruby-support#libraries --- .tool-versions | 2 +- Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.tool-versions b/.tool-versions index a19bf3ddd..ddf26c264 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ ruby 3.1.3 -bundler 2.2.33 +bundler 2.3.25 postgres 14.6 diff --git a/Gemfile.lock b/Gemfile.lock index 56f3d615f..95b6db04c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -245,4 +245,4 @@ RUBY VERSION ruby 3.1.3 BUNDLED WITH - 2.2.33 + 2.3.25 From 8055a898e2043a63dc65b534c3a062a533f41eaa Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 15 Dec 2022 13:59:42 -0800 Subject: [PATCH 0377/1107] Move enclosure download link to the heading (#678) It's kind of a pain to hunt down the download link all the way at the bottom, especially in cases where the body is longer. --- app/assets/stylesheets/application.css | 6 +++++- app/views/js/templates/_story.js.erb | 14 ++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 6b8864c83..5adda52ac 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -207,7 +207,11 @@ li.story.open .story-preview { margin-left: 20px; } -.story-keep-unread, .story-starred, .story-enclosure { +.story-enclosure { + float: right; +} + +.story-keep-unread, .story-starred { display: inline-block; cursor: pointer; -webkit-touch-callout: none; diff --git a/app/views/js/templates/_story.js.erb b/app/views/js/templates/_story.js.erb index 874520322..2c10c12fc 100644 --- a/app/views/js/templates/_story.js.erb +++ b/app/views/js/templates/_story.js.erb @@ -22,7 +22,14 @@
    -

    {{= title }}

    +

    + {{= title }} + {{ if (enclosure_url) { }} + + + + {{ } }} +

    {{= body }}
    @@ -38,11 +45,6 @@
    - {{ if (enclosure_url) { }} - - - - {{ } }} From 123e232850c5884d51cdd24688d9f63c3271fb5e Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 15 Dec 2022 15:54:01 -0800 Subject: [PATCH 0378/1107] Set timezone in app.rb (#679) This sets `Time.zone` inside `app.rb`. Currently it isn't set, which causes `delayed_job_active_record` to [go down a deprecated path][dj]. `ActiveRecord#default_timezone` is deprecated and will be removed in Rails 7.1, so setting the timezone avoids this issue and punts the problem of an outdated queueing system down the road. [dj]: https://github.com/collectiveidea/delayed_job_active_record/blob/d65b0f9900f5b0c78c341c7c0209c2d138d64ec5/lib/delayed/backend/active_record.rb#L175 --- app.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app.rb b/app.rb index 9f0d3fcb4..b312f052f 100644 --- a/app.rb +++ b/app.rb @@ -17,6 +17,7 @@ I18n.load_path += Dir[File.join(File.dirname(__FILE__), "config/locales", "*.yml").to_s] I18n.config.enforce_available_locales = false +Time.zone = ENV.fetch("TZ", "UTC") class Stringer < Sinatra::Base # need to exclude assets for sinatra assetpack, see https://github.com/stringer-rss/stringer/issues/112 From 45d06b292d23a06bd58fcfa0271b7ab3c5d8054c Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 15 Dec 2022 20:27:32 -0800 Subject: [PATCH 0379/1107] RuboCop: enable Layout/FirstMethodArgumentLineBreak (#680) --- .rubocop_todo.yml | 11 ------ app/commands/feeds/add_new_feed.rb | 8 +++-- app/commands/users/create_user.rb | 10 +++--- app/repositories/story_repository.rb | 20 ++++++----- spec/models/story_spec.rb | 40 +++++++++++---------- spec/repositories/story_repository_spec.rb | 42 +++++++++++++--------- spec/tasks/fetch_feed_spec.rb | 10 +++--- 7 files changed, 75 insertions(+), 66 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d80bd135b..4b662f2a7 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -21,17 +21,6 @@ Bundler/GemVersion: Exclude: - 'Gemfile' -# Offense count: 9 -# This cop supports safe autocorrection (--autocorrect). -Layout/FirstMethodArgumentLineBreak: - Exclude: - - 'app/commands/feeds/add_new_feed.rb' - - 'app/commands/users/create_user.rb' - - 'app/repositories/story_repository.rb' - - 'spec/models/story_spec.rb' - - 'spec/repositories/story_repository_spec.rb' - - 'spec/tasks/fetch_feed_spec.rb' - # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. diff --git a/app/commands/feeds/add_new_feed.rb b/app/commands/feeds/add_new_feed.rb index 2d7502e29..5fa5810db 100644 --- a/app/commands/feeds/add_new_feed.rb +++ b/app/commands/feeds/add_new_feed.rb @@ -9,8 +9,10 @@ def self.add(url, discoverer = FeedDiscovery.new, repo = Feed) result = discoverer.discover(url) return false unless result - repo.create(name: ContentSanitizer.sanitize(result.title), - url: result.feed_url, - last_fetched: Time.now - ONE_DAY) + repo.create( + name: ContentSanitizer.sanitize(result.title), + url: result.feed_url, + last_fetched: Time.now - ONE_DAY + ) end end diff --git a/app/commands/users/create_user.rb b/app/commands/users/create_user.rb index d3de6fe27..c295500f1 100644 --- a/app/commands/users/create_user.rb +++ b/app/commands/users/create_user.rb @@ -7,9 +7,11 @@ def initialize(repository = User) def create(password) @repo.delete_all - @repo.create(password: password, - password_confirmation: password, - setup_complete: false, - api_key: ApiKey.compute(password)) + @repo.create( + password: password, + password_confirmation: password, + setup_complete: false, + api_key: ApiKey.compute(password) + ) end end diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index 75fbdd1b9..b33ea2e70 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -8,15 +8,17 @@ class StoryRepository def self.add(entry, feed) enclosure_url = entry.enclosure_url if entry.respond_to?(:enclosure_url) - Story.create(feed: feed, - title: extract_title(entry), - permalink: extract_url(entry, feed), - enclosure_url: enclosure_url, - body: extract_content(entry), - is_read: false, - is_starred: false, - published: entry.published || Time.now, - entry_id: entry.id) + Story.create( + feed: feed, + title: extract_title(entry), + permalink: extract_url(entry, feed), + enclosure_url: enclosure_url, + body: extract_content(entry), + is_read: false, + is_starred: false, + published: entry.published || Time.now, + entry_id: entry.id + ) end def self.fetch(id) diff --git a/spec/models/story_spec.rb b/spec/models/story_spec.rb index e0674b4cc..2375d0b29 100644 --- a/spec/models/story_spec.rb +++ b/spec/models/story_spec.rb @@ -82,25 +82,27 @@ updated_at: updated_at ) - expect(story.as_json).to eq({ - body: "story body", - created_at: created_at.utc.as_json, - enclosure_url: nil, - entry_id: "5", - feed_id: feed.id, - headline: "the story title", - id: story.id, - is_read: true, - is_starred: false, - keep_unread: true, - lead: "story body", - permalink: "www.exampoo.com/perma", - pretty_date: I18n.l(published_at.utc), - published: published_at.utc.as_json, - source: "my feed", - title: "the story title", - updated_at: updated_at.utc.as_json - }.stringify_keys) + expect(story.as_json).to eq( + { + body: "story body", + created_at: created_at.utc.as_json, + enclosure_url: nil, + entry_id: "5", + feed_id: feed.id, + headline: "the story title", + id: story.id, + is_read: true, + is_starred: false, + keep_unread: true, + lead: "story body", + permalink: "www.exampoo.com/perma", + pretty_date: I18n.l(published_at.utc), + published: published_at.utc.as_json, + source: "my feed", + title: "the story title", + updated_at: updated_at.utc.as_json + }.stringify_keys + ) end end diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 19ea42bb0..10ab76f46 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -36,11 +36,13 @@ end it "sets the enclosure url when present" do - entry = instance_double(Feedjira::Parser::ITunesRSSItem, - enclosure_url: "http://example.com/audio.mp3", - title: "", - summary: "", - content: "").as_null_object + entry = instance_double( + Feedjira::Parser::ITunesRSSItem, + enclosure_url: "http://example.com/audio.mp3", + title: "", + summary: "", + content: "" + ).as_null_object allow(StoryRepository).to receive(:normalize_url) expect(Story).to receive(:create).with(hash_including(enclosure_url: "http://example.com/audio.mp3")) @@ -430,14 +432,18 @@ describe ".extract_content" do let(:entry) do - double(url: "http://mdswanson.com", - content: "Some test content") + double( + url: "http://mdswanson.com", + content: "Some test content" + ) end let(:summary_only) do - double(url: "http://mdswanson.com", - content: nil, - summary: "Dumb publisher") + double( + url: "http://mdswanson.com", + content: nil, + summary: "Dumb publisher" + ) end it "sanitizes content" do @@ -449,17 +455,21 @@ end it "expands urls" do - entry = double(url: "http://mdswanson.com", - content: nil, - summary: "Page") + entry = double( + url: "http://mdswanson.com", + content: nil, + summary: "Page" + ) expect(StoryRepository.extract_content(entry)).to eq "Page" end it "ignores URL expansion if entry url is nil" do - entry = double(url: nil, - content: nil, - summary: "Page") + entry = double( + url: nil, + content: nil, + summary: "Page" + ) expect(StoryRepository.extract_content(entry)).to eq "Page" end diff --git a/spec/tasks/fetch_feed_spec.rb b/spec/tasks/fetch_feed_spec.rb index d8fb83b22..455ff0d3f 100644 --- a/spec/tasks/fetch_feed_spec.rb +++ b/spec/tasks/fetch_feed_spec.rb @@ -4,10 +4,12 @@ describe FetchFeed do describe "#fetch" do let(:daring_fireball) do - double(id: 1, - url: "http://daringfireball.com/feed", - last_fetched: Time.new(2013, 1, 1), - stories: []) + double( + id: 1, + url: "http://daringfireball.com/feed", + last_fetched: Time.new(2013, 1, 1), + stories: [] + ) end before do From 72840ad4e220951e2f99ad4efc7d3549e749bac2 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 15 Dec 2022 20:33:46 -0800 Subject: [PATCH 0380/1107] RuboCop: enable Layout/LineEndStringConcatenationIndentation (#681) --- .rubocop_todo.yml | 8 -------- spec/helpers/url_helpers_spec.rb | 12 ++++++------ 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 4b662f2a7..78be8c1a1 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -21,14 +21,6 @@ Bundler/GemVersion: Exclude: - 'Gemfile' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: aligned, indented -Layout/LineEndStringConcatenationIndentation: - Exclude: - - 'spec/helpers/url_helpers_spec.rb' - # Offense count: 4 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. diff --git a/spec/helpers/url_helpers_spec.rb b/spec/helpers/url_helpers_spec.rb index db775744e..27a044ed1 100644 --- a/spec/helpers/url_helpers_spec.rb +++ b/spec/helpers/url_helpers_spec.rb @@ -61,12 +61,12 @@ class Helper # rubocop:disable Lint/ConstantDefinitionInBlock it "leaves the url as-is if it cannot be parsed" do weird_url = "https://github.com/aphyr/jepsen/blob/" \ - "1403f2d6e61c595bafede0d404fd4a893371c036/" \ - "elasticsearch/src/jepsen/system/elasticsearch.clj#" \ - "L161-L226.%20Then%20we'll%20write%20a%20%5Bregister%20test%5D(" \ - "https://github.com/aphyr/jepsen/blob/" \ - "1403f2d6e61c595bafede0d404fd4a893371c036/" \ - "elasticsearch/test/jepsen/system/elasticsearch_test.clj#L18-L50)" + "1403f2d6e61c595bafede0d404fd4a893371c036/" \ + "elasticsearch/src/jepsen/system/elasticsearch.clj#" \ + "L161-L226.%20Then%20we'll%20write%20a%20%5Bregister%20test%5D(" \ + "https://github.com/aphyr/jepsen/blob/" \ + "1403f2d6e61c595bafede0d404fd4a893371c036/" \ + "elasticsearch/test/jepsen/system/elasticsearch_test.clj#L18-L50)" content = "" From 6799fe0428a1a6d2ce62d0eff0d3474e1478d3d1 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 15 Dec 2022 20:43:39 -0800 Subject: [PATCH 0381/1107] RuboCop: enable Layout/MultilineAssignmentLayout (#682) --- .rubocop_todo.yml | 12 --- app/commands/feeds/export_to_opml.rb | 29 +++--- app/fever_api/read_items.rb | 11 +-- app/jobs/fetch_feed_job.rb | 11 +-- app/utils/sample_story.rb | 127 ++++++++++++++------------- 5 files changed, 91 insertions(+), 99 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 78be8c1a1..1f9ded5b6 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -21,18 +21,6 @@ Bundler/GemVersion: Exclude: - 'Gemfile' -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedTypes: block, case, class, if, kwbegin, module -# SupportedStyles: same_line, new_line -Layout/MultilineAssignmentLayout: - Exclude: - - 'app/commands/feeds/export_to_opml.rb' - - 'app/fever_api/read_items.rb' - - 'app/jobs/fetch_feed_job.rb' - - 'app/utils/sample_story.rb' - # Offense count: 4 # This cop supports safe autocorrection (--autocorrect). Layout/MultilineMethodArgumentLineBreaks: diff --git a/app/commands/feeds/export_to_opml.rb b/app/commands/feeds/export_to_opml.rb index 707274e60..38715904e 100644 --- a/app/commands/feeds/export_to_opml.rb +++ b/app/commands/feeds/export_to_opml.rb @@ -6,23 +6,24 @@ def initialize(feeds) end def to_xml # rubocop:disable Metrics/MethodLength - builder = Nokogiri::XML::Builder.new do |xml| - xml.opml(version: "1.0") do - xml.head do - xml.title "Feeds from Stringer" - end - xml.body do - @feeds.each do |feed| - xml.outline( - text: feed.name, - title: feed.name, - type: "rss", - xmlUrl: feed.url - ) + builder = + Nokogiri::XML::Builder.new do |xml| + xml.opml(version: "1.0") do + xml.head do + xml.title "Feeds from Stringer" + end + xml.body do + @feeds.each do |feed| + xml.outline( + text: feed.name, + title: feed.name, + type: "rss", + xmlUrl: feed.url + ) + end end end end - end builder.to_xml end diff --git a/app/fever_api/read_items.rb b/app/fever_api/read_items.rb index 84c94e4ae..ca26d04b9 100644 --- a/app/fever_api/read_items.rb +++ b/app/fever_api/read_items.rb @@ -8,11 +8,12 @@ def initialize(options = {}) def call(params = {}) if params.keys.include?("items") - item_ids = begin - params[:with_ids].split(",") - rescue StandardError - nil - end + item_ids = + begin + params[:with_ids].split(",") + rescue StandardError + nil + end { items: items(item_ids, params[:since_id]), diff --git a/app/jobs/fetch_feed_job.rb b/app/jobs/fetch_feed_job.rb index 4f5915a1b..77bedc4d1 100644 --- a/app/jobs/fetch_feed_job.rb +++ b/app/jobs/fetch_feed_job.rb @@ -1,6 +1,7 @@ -FetchFeedJob = Struct.new(:feed_id) do - def perform - feed = FeedRepository.fetch(feed_id) - FetchFeed.new(feed).fetch +FetchFeedJob = + Struct.new(:feed_id) do + def perform + feed = FeedRepository.fetch(feed_id) + FetchFeed.new(feed).fetch + end end -end diff --git a/app/utils/sample_story.rb b/app/utils/sample_story.rb index 0bcc41558..7c3985e40 100644 --- a/app/utils/sample_story.rb +++ b/app/utils/sample_story.rb @@ -1,74 +1,75 @@ -SampleStory = Struct.new(:source, :title, :lead, :is_read, :published) do - BODY = <<~EOS.freeze # rubocop:disable Lint/ConstantDefinitionInBlock -

    Tofu shoreditch intelligentsia umami, fashion axe photo booth - try-hard terry richardson quinoa actually fingerstache meggings fixie. Aesthetic - salvia vinyl raw denim, keffiyeh master cleanse tonx selfies mlkshk occupy twee - street art gentrify. Quinoa PBR readymade 90's. Chambray Austin aesthetic - meggings, carles vinyl intelligentsia tattooed. Keffiyeh mumblecore - fingerstache, sartorial sriracha disrupt biodiesel cred. Skateboard yr cosby - sweater, narwhal beard ethnic jean shorts aesthetic. Post-ironic flannel mlkshk, - pickled VHS wolf banjo forage portland wayfarers.

    - -

    Selfies mumblecore odd future irony DIY messenger bag. - Authentic neutra next level selvage squid. Four loko freegan occupy, tousled - vinyl leggings selvage messenger bag. Four loko wayfarers kale chips, next level - banksy banh mi umami flannel hella. Street art odd future scenester, - intelligentsia brunch fingerstache YOLO narwhal single-origin coffee tousled - tumblr pop-up four loko you probably haven't heard of them dreamcatcher. - Single-origin coffee direct trade retro biodiesel, truffaut fanny pack portland - blue bottle scenester bushwick. Skateboard squid fanny pack bushwick, photo - booth vice literally.

    - EOS +SampleStory = + Struct.new(:source, :title, :lead, :is_read, :published) do + BODY = <<~EOS.freeze # rubocop:disable Lint/ConstantDefinitionInBlock +

    Tofu shoreditch intelligentsia umami, fashion axe photo booth + try-hard terry richardson quinoa actually fingerstache meggings fixie. Aesthetic + salvia vinyl raw denim, keffiyeh master cleanse tonx selfies mlkshk occupy twee + street art gentrify. Quinoa PBR readymade 90's. Chambray Austin aesthetic + meggings, carles vinyl intelligentsia tattooed. Keffiyeh mumblecore + fingerstache, sartorial sriracha disrupt biodiesel cred. Skateboard yr cosby + sweater, narwhal beard ethnic jean shorts aesthetic. Post-ironic flannel mlkshk, + pickled VHS wolf banjo forage portland wayfarers.

    + +

    Selfies mumblecore odd future irony DIY messenger bag. + Authentic neutra next level selvage squid. Four loko freegan occupy, tousled + vinyl leggings selvage messenger bag. Four loko wayfarers kale chips, next level + banksy banh mi umami flannel hella. Street art odd future scenester, + intelligentsia brunch fingerstache YOLO narwhal single-origin coffee tousled + tumblr pop-up four loko you probably haven't heard of them dreamcatcher. + Single-origin coffee direct trade retro biodiesel, truffaut fanny pack portland + blue bottle scenester bushwick. Skateboard squid fanny pack bushwick, photo + booth vice literally.

    + EOS - def id - -1 * rand(100) - end + def id + -1 * rand(100) + end - def headline - title - end + def headline + title + end - def permalink - "#" - end + def permalink + "#" + end - def lead - "Tofu shoreditch intelligentsia umami, fashion axe photo booth try-hard" - end + def lead + "Tofu shoreditch intelligentsia umami, fashion axe photo booth try-hard" + end - def body - BODY - end + def body + BODY + end - def is_read # rubocop:disable Naming/PredicateName - false - end + def is_read # rubocop:disable Naming/PredicateName + false + end - def keep_unread - false - end + def keep_unread + false + end - def is_starred # rubocop:disable Naming/PredicateName - false - end + def is_starred # rubocop:disable Naming/PredicateName + false + end - def published - Time.now - end + def published + Time.now + end - def as_json(_options = {}) - { - id: id, - headline: headline, - lead: lead, - source: source, - title: title, - pretty_date: published.strftime("%A, %B %d"), - body: body, - permalink: permalink, - is_read: is_read, - is_starred: is_starred, - keep_unread: keep_unread - } + def as_json(_options = {}) + { + id: id, + headline: headline, + lead: lead, + source: source, + title: title, + pretty_date: published.strftime("%A, %B %d"), + body: body, + permalink: permalink, + is_read: is_read, + is_starred: is_starred, + keep_unread: keep_unread + } + end end -end From 18664ffbe0cac8a71e658720357b7407413f34c0 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 15 Dec 2022 20:48:17 -0800 Subject: [PATCH 0382/1107] RuboCop: enable Layout/MultilineMethodArgumentLineBreaks (#683) --- .rubocop_todo.yml | 8 -------- app/controllers/debug_controller.rb | 9 +++++---- spec/controllers/sessions_controller_spec.rb | 4 ++-- spec/javascript/test_controller.rb | 10 ++++++---- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 1f9ded5b6..59acfd8d7 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -21,14 +21,6 @@ Bundler/GemVersion: Exclude: - 'Gemfile' -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -Layout/MultilineMethodArgumentLineBreaks: - Exclude: - - 'app/controllers/debug_controller.rb' - - 'spec/controllers/sessions_controller_spec.rb' - - 'spec/javascript/test_controller.rb' - # Offense count: 47 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: InspectBlocks. diff --git a/app/controllers/debug_controller.rb b/app/controllers/debug_controller.rb index f38594201..018a29d4d 100644 --- a/app/controllers/debug_controller.rb +++ b/app/controllers/debug_controller.rb @@ -2,10 +2,11 @@ class Stringer < Sinatra::Base get "/debug" do - erb :debug, locals: { - queued_jobs_count: Delayed::Job.count, - pending_migrations: MigrationStatus.new.pending_migrations - } + erb :debug, + locals: { + queued_jobs_count: Delayed::Job.count, + pending_migrations: MigrationStatus.new.pending_migrations + } end get "/heroku" do diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index cb67be117..87ef61b18 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -37,8 +37,8 @@ it "redirects to the previous path when present" do allow(SignInUser).to receive(:sign_in).and_return(double(id: 1)) - post "/login", { password: "the-password" }, - "rack.session" => { redirect_to: "/archive" } + params = { password: "the-password" } + post "/login", params, "rack.session" => { redirect_to: "/archive" } expect(session[:redirect_to]).to be_nil expect(URI.parse(last_response.location).path).to eq "/archive" diff --git a/spec/javascript/test_controller.rb b/spec/javascript/test_controller.rb index c79b0993e..7a13e99d8 100644 --- a/spec/javascript/test_controller.rb +++ b/spec/javascript/test_controller.rb @@ -4,10 +4,12 @@ def self.test_path(*chunks) end get "/test" do - erb File.read(self.class.test_path("support", "views", "index.erb")), layout: false, locals: { - js_files: js_files, - js_templates: js_templates - } + erb File.read(self.class.test_path("support", "views", "index.erb")), + layout: false, + locals: { + js_files: js_files, + js_templates: js_templates + } end get "/spec/*" do From 2606402f3e4e603b5784079fbe6e53f722c5451b Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 15 Dec 2022 21:13:11 -0800 Subject: [PATCH 0383/1107] RuboCop: reduce line length to 110 (#684) --- .rubocop.yml | 2 +- spec/controllers/debug_controller_spec.rb | 4 +++- spec/fever_api_spec.rb | 8 ++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index a616fda72..fb86053c1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,7 +12,7 @@ AllCops: - 'vendor/**/*' Layout/LineLength: - Max: 120 + Max: 110 Metrics/BlockLength: Exclude: diff --git a/spec/controllers/debug_controller_spec.rb b/spec/controllers/debug_controller_spec.rb index 3c435ef9a..864ae989c 100644 --- a/spec/controllers/debug_controller_spec.rb +++ b/spec/controllers/debug_controller_spec.rb @@ -11,7 +11,9 @@ stub_const("Delayed::Job", delayed_job) migration_status_instance = double "migration_status_instance" - allow(migration_status_instance).to receive(:pending_migrations).and_return ["Migration B - 2", "Migration C - 3"] + allow(migration_status_instance) + .to receive(:pending_migrations) + .and_return(["Migration B - 2", "Migration C - 3"]) migration_status = double "MigrationStatus" allow(migration_status).to receive(:new).and_return(migration_status_instance) stub_const("MigrationStatus", migration_status) diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index 011d0eb43..74353690b 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -223,7 +223,9 @@ def make_request(extra_headers = {}) end it "commands to mark group as read" do - expect(MarkGroupAsRead).to receive(:new).with("10", "1375080946").and_return(double(mark_group_as_read: true)) + expect(MarkGroupAsRead) + .to receive(:new).with("10", "1375080946") + .and_return(double(mark_group_as_read: true)) make_request(mark: "group", as: "read", id: 10, before: 1375080946) @@ -232,7 +234,9 @@ def make_request(extra_headers = {}) end it "commands to mark entire feed as read" do - expect(MarkFeedAsRead).to receive(:new).with("20", "1375080945").and_return(double(mark_feed_as_read: true)) + expect(MarkFeedAsRead) + .to receive(:new).with("20", "1375080945") + .and_return(double(mark_feed_as_read: true)) make_request(mark: "feed", as: "read", id: 20, before: 1375080945) From 7af842b8891bef746f2cc42416a9418bf7e1517f Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Fri, 16 Dec 2022 11:37:57 -0800 Subject: [PATCH 0384/1107] Security: enforce SSL in tests (#687) I had to add a monkey patch for `Rack::Test` as they don't seem to have a good way to enable SSL by default otherwise. --- spec/spec_helper.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8c57bddd5..db68aa468 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,5 @@ ENV["RACK_ENV"] = "test" +ENV["ENFORCE_SSL"] = "true" require "capybara" require "capybara/server" @@ -17,6 +18,19 @@ Capybara.server = :puma, { Silent: true } +module Rack + module Test + class Session + alias old_custom_request custom_request + + def custom_request(method, path, params = {}, env = {}, &) + env["HTTPS"] = "on" + old_custom_request(method, path, params, env, &) + end + end + end +end + RSpec.configure do |config| config.include Rack::Test::Methods config.include RSpecHtmlMatchers From 5686409ceffd279e3dffb67610f20e0dd302d364 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Fri, 16 Dec 2022 11:45:05 -0800 Subject: [PATCH 0385/1107] RuboCop: reduce line length to 100 (#685) --- .rubocop.yml | 2 +- app.rb | 2 +- app/repositories/story_repository.rb | 5 ++++- spec/commands/feeds/add_new_feed_spec.rb | 4 +++- spec/commands/feeds/import_from_opml_spec.rb | 6 ++++-- spec/commands/stories/mark_group_as_read_spec.rb | 4 +++- spec/controllers/feeds_controller_spec.rb | 14 ++++++++++++-- spec/controllers/stories_controller_spec.rb | 3 ++- spec/fever_api_spec.rb | 3 ++- spec/models/migration_status_spec.rb | 3 ++- spec/repositories/story_repository_spec.rb | 7 ++++++- spec/tasks/fetch_feed_spec.rb | 4 +++- spec/tasks/remove_old_stories_spec.rb | 3 ++- spec/utils/i18n_support_spec.rb | 3 ++- 14 files changed, 47 insertions(+), 16 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index fb86053c1..00ada93d0 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,7 +12,7 @@ AllCops: - 'vendor/**/*' Layout/LineLength: - Max: 110 + Max: 100 Metrics/BlockLength: Exclude: diff --git a/app.rb b/app.rb index b312f052f..4bce33b72 100644 --- a/app.rb +++ b/app.rb @@ -21,7 +21,7 @@ class Stringer < Sinatra::Base # need to exclude assets for sinatra assetpack, see https://github.com/stringer-rss/stringer/issues/112 - use Rack::SSL, exclude: ->(env) { env["PATH_INFO"] =~ %r{^/(js|css|img)} } if ENV["ENFORCE_SSL"] == "true" + use Rack::SSL, exclude: ->(env) { env["PATH_INFO"] =~ %r{^/(js|css|img)} } register Sinatra::ActiveRecordExtension register Sinatra::Flash diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index b33ea2e70..a1c051e7f 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -118,7 +118,10 @@ def self.extract_title(entry) def self.samples [ SampleStory.new("Darin' Fireballs", "Why you should trade your firstborn for a Retina iPad"), - SampleStory.new("TechKrunch", "SugarGlidr raises $1.2M Series A for Social Network for Photo Filters"), + SampleStory.new( + "TechKrunch", + "SugarGlidr raises $1.2M Series A for Social Network for Photo Filters" + ), SampleStory.new("Lambda Da Ultimate", "Flimsy types are the new hotness") ] end diff --git a/spec/commands/feeds/add_new_feed_spec.rb b/spec/commands/feeds/add_new_feed_spec.rb index 22fde4e76..54526730d 100644 --- a/spec/commands/feeds/add_new_feed_spec.rb +++ b/spec/commands/feeds/add_new_feed_spec.rb @@ -29,7 +29,9 @@ end context "title includes a script tag" do - let(:feed_result) { double(title: "foobar", feed_url: feed.url) } + let(:feed_result) do + double(title: "foobar", feed_url: feed.url) + end it "deletes the script tag from the title" do allow(repo).to receive(:create) diff --git a/spec/commands/feeds/import_from_opml_spec.rb b/spec/commands/feeds/import_from_opml_spec.rb index 49661795a..6516ece9e 100644 --- a/spec/commands/feeds/import_from_opml_spec.rb +++ b/spec/commands/feeds/import_from_opml_spec.rb @@ -3,7 +3,9 @@ app_require "commands/feeds/import_from_opml" describe ImportFromOpml do - let(:subscriptions) { File.open(File.expand_path("../../support/files/subscriptions.xml", __dir__)) } + let(:subscriptions) do + File.open(File.expand_path("../../support/files/subscriptions.xml", __dir__)) + end def import described_class.import(subscriptions) @@ -15,7 +17,7 @@ def import end let(:group1) { Group.find_by_name("Football News") } - let(:group2) { Group.find_by_name("RoR") } + let(:group2) { Group.find_by_name("RoR") } context "adding group_id for existing feeds" do let!(:feed1) do diff --git a/spec/commands/stories/mark_group_as_read_spec.rb b/spec/commands/stories/mark_group_as_read_spec.rb index 84d68b81a..452b2446d 100644 --- a/spec/commands/stories/mark_group_as_read_spec.rb +++ b/spec/commands/stories/mark_group_as_read_spec.rb @@ -15,7 +15,9 @@ def run_command(group_id) it "marks group as read" do command = run_command(2) expect(stories).to receive(:update_all).with(is_read: true) - expect(repo).to receive(:fetch_unread_by_timestamp_and_group).with(timestamp, 2).and_return(stories) + expect(repo).to receive(:fetch_unread_by_timestamp_and_group) + .with(timestamp, 2).and_return(stories) + command.mark_group_as_read end diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index 3917ba408..3319b98d6 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -133,7 +133,9 @@ end describe "POST /feeds/import" do - let(:opml_file) { Rack::Test::UploadedFile.new("spec/sample_data/subscriptions.xml", "application/xml") } + let(:opml_file) do + Rack::Test::UploadedFile.new("spec/sample_data/subscriptions.xml", "application/xml") + end it "parse OPML and starts fetching" do expect(ImportFromOpml).to receive(:import).once @@ -155,8 +157,16 @@ get "/feeds/export" expect(last_response.body).to eq some_xml + end + + it "responds with OPML headers" do + expect_any_instance_of(ExportToOpml).to receive(:to_xml).and_return(some_xml) + + get "/feeds/export" + expect(last_response.header["Content-Type"]).to include "application/xml" - expect(last_response.header["Content-Disposition"]).to eq("attachment; filename=\"stringer.opml\"") + expect(last_response.header["Content-Disposition"]) + .to eq("attachment; filename=\"stringer.opml\"") end end end diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index 60ab88c0c..87169ceb7 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -160,7 +160,8 @@ describe "GET /feed/:feed_id" do it "looks for a particular feed" do - expect(FeedRepository).to receive(:fetch).with(story_one.feed.id.to_s).and_return(story_one.feed) + expect(FeedRepository).to receive(:fetch) + .with(story_one.feed.id.to_s).and_return(story_one.feed) expect(StoryRepository).to receive(:feed).with(story_one.feed.id.to_s).and_return([story_one]) get "/feed/#{story_one.feed.id}" diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index 74353690b..d5327a55a 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -214,7 +214,8 @@ def make_request(extra_headers = {}) end it "commands to unsave story" do - expect(MarkAsUnstarred).to receive(:new).with("10").and_return(double(mark_as_unstarred: true)) + expect(MarkAsUnstarred).to receive(:new) + .with("10").and_return(double(mark_as_unstarred: true)) make_request(mark: "item", as: "unsaved", id: 10) diff --git a/spec/models/migration_status_spec.rb b/spec/models/migration_status_spec.rb index e05961abc..bb3791d5d 100644 --- a/spec/models/migration_status_spec.rb +++ b/spec/models/migration_status_spec.rb @@ -15,7 +15,8 @@ allow(migrator).to receive(:migrations_path) allow(migrator).to receive(:current_version).and_return 1 - expect(MigrationStatus.new(migrator).pending_migrations).to eq ["Migration B - 2", "Migration C - 3"] + expect(MigrationStatus.new(migrator).pending_migrations) + .to eq(["Migration B - 2", "Migration C - 3"]) end end end diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 10ab76f46..47e6af66c 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -51,7 +51,12 @@ end it "does not set the enclosure url when not present" do - entry = instance_double(Feedjira::Parser::RSSEntry, title: "", summary: "", content: "").as_null_object + entry = instance_double( + Feedjira::Parser::RSSEntry, + title: "", + summary: "", + content: "" + ).as_null_object allow(StoryRepository).to receive(:normalize_url) expect(Story).to receive(:create).with(hash_including(enclosure_url: nil)) diff --git a/spec/tasks/fetch_feed_spec.rb b/spec/tasks/fetch_feed_spec.rb index 455ff0d3f..1c1ff0f8a 100644 --- a/spec/tasks/fetch_feed_spec.rb +++ b/spec/tasks/fetch_feed_spec.rb @@ -52,7 +52,9 @@ let(:fake_client) { class_spy(HTTParty) } let(:fake_parser) { class_double(Feedjira, parse: fake_feed) } - before { allow_any_instance_of(FindNewStories).to receive(:new_stories).and_return([new_story]) } + before do + allow_any_instance_of(FindNewStories).to receive(:new_stories).and_return([new_story]) + end it "should only add posts that are new" do expect(StoryRepository).to receive(:add).with(new_story, daring_fireball) diff --git a/spec/tasks/remove_old_stories_spec.rb b/spec/tasks/remove_old_stories_spec.rb index 23271155e..e038ce10b 100644 --- a/spec/tasks/remove_old_stories_spec.rb +++ b/spec/tasks/remove_old_stories_spec.rb @@ -12,7 +12,8 @@ it "should pass along the number of days to the story repository query" do allow(RemoveOldStories).to receive(:pruned_feeds) { [] } - expect(StoryRepository).to receive(:unstarred_read_stories_older_than).with(7).and_return(stories_mock) + expect(StoryRepository).to receive(:unstarred_read_stories_older_than) + .with(7).and_return(stories_mock) RemoveOldStories.remove!(7) end diff --git a/spec/utils/i18n_support_spec.rb b/spec/utils/i18n_support_spec.rb index edca8a19f..e60e77b2d 100644 --- a/spec/utils/i18n_support_spec.rb +++ b/spec/utils/i18n_support_spec.rb @@ -29,7 +29,8 @@ let(:locale) { "xx" } it "should not find localization strings" do - expect(I18n.t("layout.title", locale: ENV["LOCALE"].to_sym)).not_to eq "stringer | your rss buddy" + expect(I18n.t("layout.title", locale: ENV["LOCALE"].to_sym)) + .not_to eq "stringer | your rss buddy" end end end From a5ab61feadcaaae1d562b1fe99075f6a46b862ba Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Fri, 16 Dec 2022 12:22:04 -0800 Subject: [PATCH 0386/1107] RuboCop: reduce line length to 95 (#688) --- .rubocop.yml | 2 +- app/commands/feeds/find_new_stories.rb | 5 +++- app/commands/stories/mark_group_as_read.rb | 5 +++- app/fever_api/write_mark_feed.rb | 4 +++- app/fever_api/write_mark_group.rb | 4 +++- app/repositories/story_repository.rb | 5 +++- config/puma.rb | 8 +++++-- spec/controllers/feeds_controller_spec.rb | 23 +++++++++++++++---- spec/controllers/first_run_controller_spec.rb | 3 ++- spec/controllers/stories_controller_spec.rb | 3 ++- spec/fever_api/read_items_spec.rb | 3 ++- spec/fever_api_spec.rb | 15 ++++++++---- 12 files changed, 60 insertions(+), 20 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 00ada93d0..d339f6614 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,7 +12,7 @@ AllCops: - 'vendor/**/*' Layout/LineLength: - Max: 100 + Max: 95 Metrics/BlockLength: Exclude: diff --git a/app/commands/feeds/find_new_stories.rb b/app/commands/feeds/find_new_stories.rb index 609198562..0aabad8f9 100644 --- a/app/commands/feeds/find_new_stories.rb +++ b/app/commands/feeds/find_new_stories.rb @@ -15,7 +15,10 @@ def new_stories @raw_feed.entries.each do |story| break if @latest_entry_id && story.id == @latest_entry_id - next if story_age_exceeds_threshold?(story) || StoryRepository.exists?(story.id, @feed_id) + next if story_age_exceeds_threshold?(story) || StoryRepository.exists?( + story.id, + @feed_id + ) stories << story end diff --git a/app/commands/stories/mark_group_as_read.rb b/app/commands/stories/mark_group_as_read.rb index 286b0821c..ff24c9ce8 100644 --- a/app/commands/stories/mark_group_as_read.rb +++ b/app/commands/stories/mark_group_as_read.rb @@ -16,7 +16,10 @@ def mark_group_as_read if [KINDLING_GROUP_ID, SPARKS_GROUP_ID].include?(@group_id.to_i) @repo.fetch_unread_by_timestamp(@timestamp).update_all(is_read: true) elsif @group_id.to_i > 0 - @repo.fetch_unread_by_timestamp_and_group(@timestamp, @group_id).update_all(is_read: true) + @repo.fetch_unread_by_timestamp_and_group( + @timestamp, + @group_id + ).update_all(is_read: true) end end end diff --git a/app/fever_api/write_mark_feed.rb b/app/fever_api/write_mark_feed.rb index 1c4d72961..b4b582e6b 100644 --- a/app/fever_api/write_mark_feed.rb +++ b/app/fever_api/write_mark_feed.rb @@ -7,7 +7,9 @@ def initialize(options = {}) end def call(params = {}) - @marker_class.new(params[:id], params[:before]).mark_feed_as_read if params[:mark] == "feed" + if params[:mark] == "feed" + @marker_class.new(params[:id], params[:before]).mark_feed_as_read + end {} end diff --git a/app/fever_api/write_mark_group.rb b/app/fever_api/write_mark_group.rb index 3ef39a0e1..7ccbe127b 100644 --- a/app/fever_api/write_mark_group.rb +++ b/app/fever_api/write_mark_group.rb @@ -7,7 +7,9 @@ def initialize(options = {}) end def call(params = {}) - @marker_class.new(params[:id], params[:before]).mark_group_as_read if params[:mark] == "group" + if params[:mark] == "group" + @marker_class.new(params[:id], params[:before]).mark_group_as_read + end {} end diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index a1c051e7f..a85019a8a 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -117,7 +117,10 @@ def self.extract_title(entry) def self.samples [ - SampleStory.new("Darin' Fireballs", "Why you should trade your firstborn for a Retina iPad"), + SampleStory.new( + "Darin' Fireballs", + "Why you should trade your firstborn for a Retina iPad" + ), SampleStory.new( "TechKrunch", "SugarGlidr raises $1.2M Series A for Social Network for Photo Filters" diff --git a/config/puma.rb b/config/puma.rb index 0e079d63c..24fa3537c 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -16,7 +16,9 @@ # as there's no need for the master process to hold a connection ActiveRecord::Base.connection.disconnect! if defined?(ActiveRecord::Base) - @delayed_job_pid ||= spawn("bundle exec rake work_jobs") unless ENV["WORKER_EMBEDDED"] == "false" + unless ENV["WORKER_EMBEDDED"] == "false" + @delayed_job_pid ||= spawn("bundle exec rake work_jobs") + end sleep 1 end @@ -30,5 +32,7 @@ end on_worker_shutdown do - Process.kill("QUIT", @delayed_job_pid) if !ENV["RACK_ENV"] || ENV["RACK_ENV"] == "development" + if !ENV["RACK_ENV"] || ENV["RACK_ENV"] == "development" + Process.kill("QUIT", @delayed_job_pid) + end end diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index 3319b98d6..3f61f6efa 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -38,11 +38,25 @@ end end + def mock_feed(feed, name, url, group_id = nil) + expect(FeedRepository).to receive(:fetch).with("123").and_return(feed) + expect(FeedRepository).to receive(:update_feed).with(feed, name, url, group_id) + end + + def params(feed, **overrides) + { + feed_id: feed.id, + feed_name: feed.name, + feed_url: feed.url, + group_id: feed.group_id, + **overrides + } + end + describe "PUT /feeds/:feed_id" do it "updates a feed given the id" do feed = FeedFactory.build(url: "example.com/atom") - expect(FeedRepository).to receive(:fetch).with("123").and_return(feed) - expect(FeedRepository).to receive(:update_feed).with(feed, "Test", "example.com/feed", nil) + mock_feed(feed, "Test", "example.com/feed") put "/feeds/123", feed_id: "123", feed_name: "Test", feed_url: "example.com/feed" @@ -51,10 +65,9 @@ it "updates a feed group given the id" do feed = FeedFactory.build(url: "example.com/atom") - expect(FeedRepository).to receive(:fetch).with("123").and_return(feed) - expect(FeedRepository).to receive(:update_feed).with(feed, feed.name, feed.url, "321") + mock_feed(feed, feed.name, feed.url, "321") - put "/feeds/123", feed_id: "123", feed_name: feed.name, feed_url: feed.url, group_id: "321" + put "/feeds/123", **params(feed, feed_id: "123", group_id: "321") expect(last_response).to be_redirect end diff --git a/spec/controllers/first_run_controller_spec.rb b/spec/controllers/first_run_controller_spec.rb index 878cc3037..b9ec24a93 100644 --- a/spec/controllers/first_run_controller_spec.rb +++ b/spec/controllers/first_run_controller_spec.rb @@ -37,7 +37,8 @@ end it "accepts confirmed passwords and redirects to next step" do - expect_any_instance_of(CreateUser).to receive(:create).with("foo").and_return(double(id: 1)) + expect_any_instance_of(CreateUser) + .to receive(:create).with("foo").and_return(double(id: 1)) post "/setup/password", password: "foo", password_confirmation: "foo" diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index 87169ceb7..234dcc024 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -162,7 +162,8 @@ it "looks for a particular feed" do expect(FeedRepository).to receive(:fetch) .with(story_one.feed.id.to_s).and_return(story_one.feed) - expect(StoryRepository).to receive(:feed).with(story_one.feed.id.to_s).and_return([story_one]) + expect(StoryRepository) + .to receive(:feed).with(story_one.feed.id.to_s).and_return([story_one]) get "/feed/#{story_one.feed.id}" end diff --git a/spec/fever_api/read_items_spec.rb b/spec/fever_api/read_items_spec.rb index a124341d0..ad8c167c7 100644 --- a/spec/fever_api/read_items_spec.rb +++ b/spec/fever_api/read_items_spec.rb @@ -31,7 +31,8 @@ double("story", as_fever_json: { id: 5 }), double("story", as_fever_json: { id: 7 }) ] - expect(story_repository).to receive(:unread_since_id).with(3).and_return(unread_since_stories) + expect(story_repository) + .to receive(:unread_since_id).with(3).and_return(unread_since_stories) unread_stories = [ double("story", as_fever_json: { id: 2 }), double("story", as_fever_json: { id: 5 }), diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index d5327a55a..ebeeac78e 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -102,7 +102,7 @@ def make_request(extra_headers = {}) favicons: [ { id: 0, - data: "image/gif;base64,R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" + data: a_string_including("image/gif;base64") } ] ) @@ -136,7 +136,8 @@ def make_request(extra_headers = {}) end it "returns stories ids when 'items' header is provided along with 'with_ids'" do - expect(StoryRepository).to receive(:fetch_by_ids).twice.with(["5"]).and_return([story_one]) + expect(StoryRepository) + .to receive(:fetch_by_ids).twice.with(["5"]).and_return([story_one]) make_request(items: nil, with_ids: 5) @@ -169,7 +170,12 @@ def make_request(extra_headers = {}) end it "returns starred items when 'saved_item_ids' header is provided" do - expect(Story).to receive(:where).with(is_starred: true).and_return([story_one, story_two]) + expect(Story).to receive(:where).with(is_starred: true).and_return( + [ + story_one, + story_two + ] + ) make_request(saved_item_ids: nil) @@ -205,7 +211,8 @@ def make_request(extra_headers = {}) end it "commands to save story" do - expect(MarkAsStarred).to receive(:new).with("10").and_return(double(mark_as_starred: true)) + expect(MarkAsStarred) + .to receive(:new).with("10").and_return(double(mark_as_starred: true)) make_request(mark: "item", as: "saved", id: 10) From 5c5a45d520878b784b980145fa27d45d03659b9b Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 17 Dec 2022 14:35:20 -0800 Subject: [PATCH 0387/1107] Rearrange gemfile (#691) Put common gems ahead of environment based ones. --- Gemfile | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Gemfile b/Gemfile index 6174e4a96..a1dce645d 100644 --- a/Gemfile +++ b/Gemfile @@ -2,26 +2,6 @@ ruby_version_file = File.expand_path(".ruby-version", __dir__) ruby File.read(ruby_version_file).chomp if File.readable?(ruby_version_file) source "https://rubygems.org" -group :development do - gem "rubocop", require: false - gem "rubocop-rails", require: false - gem "rubocop-rake", require: false - gem "rubocop-rspec", require: false -end - -group :development, :test do - gem "capybara" - gem "coveralls_reborn", require: false - gem "faker" - gem "pry-byebug" - gem "rack-test" - gem "rspec" - gem "rspec-html-matchers" - gem "shotgun" - gem "simplecov" - gem "timecop" -end - gem "activerecord" gem "bcrypt" gem "delayed_job" @@ -48,3 +28,23 @@ gem "sprockets-helpers" gem "thread" gem "uglifier" gem "will_paginate" + +group :development do + gem "rubocop", require: false + gem "rubocop-rails", require: false + gem "rubocop-rake", require: false + gem "rubocop-rspec", require: false +end + +group :development, :test do + gem "capybara" + gem "coveralls_reborn", require: false + gem "faker" + gem "pry-byebug" + gem "rack-test" + gem "rspec" + gem "rspec-html-matchers" + gem "shotgun" + gem "simplecov" + gem "timecop" +end From 24a22e806462f0800fe2766e0c669851accf65e8 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 17 Dec 2022 15:25:03 -0800 Subject: [PATCH 0388/1107] Put SSL configuration back (#692) This got lost in a recent change. --- app.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app.rb b/app.rb index 4bce33b72..80a22b60b 100644 --- a/app.rb +++ b/app.rb @@ -21,7 +21,9 @@ class Stringer < Sinatra::Base # need to exclude assets for sinatra assetpack, see https://github.com/stringer-rss/stringer/issues/112 - use Rack::SSL, exclude: ->(env) { env["PATH_INFO"] =~ %r{^/(js|css|img)} } + if ENV["ENFORCE_SSL"] == "true" + use Rack::SSL, exclude: ->(env) { env["PATH_INFO"] =~ %r{^/(js|css|img)} } + end register Sinatra::ActiveRecordExtension register Sinatra::Flash From 0a38c921a48adb167d370e03ead6d3d6bb82fc5b Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 17 Dec 2022 15:48:24 -0800 Subject: [PATCH 0389/1107] Deps: introduce Rails gem (#693) **What** This adds the Rails gem and removes dependencies from our Gemfile that are depended on by Rails. **Why** This is a step in the direction of switching the app over from Sinatra to Rails. --- .rubocop_todo.yml | 61 +++++++++++++------------ Gemfile | 9 +--- Gemfile.lock | 114 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 141 insertions(+), 43 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 59acfd8d7..c32b4ef89 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,19 +1,19 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 400` -# on 2022-10-24 17:51:19 UTC using RuboCop version 1.36.0. +# on 2022-12-17 23:28:03 UTC using RuboCop version 1.40.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 40 +# Offense count: 34 # Configuration parameters: Include, IgnoredGems, OnlyFor. # Include: **/*.gemfile, **/Gemfile, **/gems.rb Bundler/GemComment: Exclude: - 'Gemfile' -# Offense count: 39 +# Offense count: 32 # Configuration parameters: EnforcedStyle, Include, AllowedGems. # SupportedStyles: required, forbidden # Include: **/*.gemfile, **/Gemfile, **/gems.rb @@ -21,30 +21,19 @@ Bundler/GemVersion: Exclude: - 'Gemfile' -# Offense count: 47 +# Offense count: 25 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: InspectBlocks. Layout/RedundantLineBreak: Exclude: - - 'Rakefile' - - 'app/commands/feeds/add_new_feed.rb' - 'app/commands/feeds/export_to_opml.rb' - - 'app/repositories/feed_repository.rb' - 'app/repositories/story_repository.rb' - 'app/utils/content_sanitizer.rb' - - 'config/asset_pipeline.rb' - - 'spec/commands/find_new_stories_spec.rb' - - 'spec/controllers/sessions_controller_spec.rb' - 'spec/factories/user_factory.rb' - 'spec/factories/users.rb' - - 'spec/fever_api/read_feeds_groups_spec.rb' - 'spec/fever_api/read_feeds_spec.rb' - - 'spec/fever_api/read_items_spec.rb' - - 'spec/fever_api_spec.rb' - 'spec/helpers/url_helpers_spec.rb' - 'spec/integration/feed_importing_spec.rb' - - 'spec/models/feed_spec.rb' - - 'spec/models/story_spec.rb' - 'spec/repositories/story_repository_spec.rb' - 'spec/tasks/fetch_feed_spec.rb' @@ -64,7 +53,7 @@ Lint/AmbiguousOperatorPrecedence: - 'spec/factories/feed_factory.rb' - 'spec/factories/group_factory.rb' -# Offense count: 766 +# Offense count: 776 # Configuration parameters: Only, Ignore. Lint/ConstantResolution: Exclude: @@ -298,7 +287,7 @@ RSpec/AlignRightLetBrace: - 'spec/tasks/fetch_feeds_spec.rb' - 'spec/utils/feed_discovery_spec.rb' -# Offense count: 5 +# Offense count: 6 RSpec/AnyInstance: Exclude: - 'spec/controllers/feeds_controller_spec.rb' @@ -338,7 +327,7 @@ RSpec/DescribeClass: - 'spec/integration/feed_importing_spec.rb' - 'spec/utils/i18n_support_spec.rb' -# Offense count: 141 +# Offense count: 145 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SkipBlocks, EnforcedStyle. # SupportedStyles: described_class, explicit @@ -400,7 +389,7 @@ RSpec/EmptyLineAfterHook: Exclude: - 'spec/controllers/stories_controller_spec.rb' -# Offense count: 50 +# Offense count: 56 # Configuration parameters: Max, CountAsOne. RSpec/ExampleLength: Exclude: @@ -428,7 +417,8 @@ RSpec/ExampleLength: # Offense count: 18 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: CustomTransform, IgnoredWords. +# Configuration parameters: CustomTransform, IgnoredWords, DisallowedExamples. +# DisallowedExamples: works RSpec/ExampleWording: Exclude: - 'spec/commands/find_new_stories_spec.rb' @@ -562,7 +552,7 @@ RSpec/MessageSpies: - 'spec/tasks/remove_old_stories_spec.rb' - 'spec/utils/feed_discovery_spec.rb' -# Offense count: 97 +# Offense count: 96 # Configuration parameters: Max. RSpec/MultipleExpectations: Exclude: @@ -607,7 +597,8 @@ RSpec/MultipleMemoizedHelpers: - 'spec/utils/feed_discovery_spec.rb' # Offense count: 28 -# Configuration parameters: IgnoreSharedExamples. +# Configuration parameters: EnforcedStyle, IgnoreSharedExamples. +# SupportedStyles: always, named_only RSpec/NamedSubject: Exclude: - 'spec/fever_api/read_favicons_spec.rb' @@ -631,6 +622,8 @@ RSpec/NestedGroups: - 'spec/integration/feed_importing_spec.rb' # Offense count: 2 +# Configuration parameters: AllowedPatterns. +# AllowedPatterns: ^expect_, ^assert_ RSpec/NoExpectationExample: Exclude: - 'spec/commands/stories/mark_group_as_read_spec.rb' @@ -711,12 +704,21 @@ RSpec/VerifiedDoubles: - 'spec/tasks/remove_old_stories_spec.rb' - 'spec/utils/feed_discovery_spec.rb' +# Offense count: 2 +# Configuration parameters: Database, Include. +# SupportedDatabases: mysql, postgresql +# Include: db/migrate/*.rb +Rails/BulkChangeTable: + Exclude: + - 'db/migrate/20130412185253_add_new_fields_to_stories.rb' + - 'db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb' + # Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Whitelist, AllowedMethods, AllowedReceivers. -# Whitelist: find_by_sql -# AllowedMethods: find_by_sql -# AllowedReceivers: Gem::Specification +# Whitelist: find_by_sql, find_by_token_for +# AllowedMethods: find_by_sql, find_by_token_for +# AllowedReceivers: Gem::Specification, page Rails/DynamicFindBy: Exclude: - 'spec/commands/feeds/import_from_opml_spec.rb' @@ -728,7 +730,7 @@ Rails/HasManyOrHasOneDependent: Exclude: - 'app/models/group.rb' -# Offense count: 28 +# Offense count: 27 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Include. # Include: spec/**/*, test/**/* @@ -803,7 +805,7 @@ Rails/SkipsModelValidations: - 'db/migrate/20140421224454_fix_invalid_unicode.rb' - 'db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb' -# Offense count: 27 +# Offense count: 26 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: strict, flexible @@ -1034,7 +1036,7 @@ Style/FrozenStringLiteralComment: - 'spec/utils/i18n_support_spec.rb' - 'spec/utils/opml_parser_spec.rb' -# Offense count: 85 +# Offense count: 86 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys @@ -1080,7 +1082,7 @@ Style/InlineComment: Exclude: - 'app/utils/opml_parser.rb' -# Offense count: 698 +# Offense count: 699 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: IgnoreMacros, AllowedMethods, IgnoredMethods, AllowedPatterns, IgnoredPatterns, IncludedMacros, AllowParenthesesInMultilineCall, AllowParenthesesInChaining, AllowParenthesesInCamelCaseMethod, AllowParenthesesInStringInterpolation, EnforcedStyle. # SupportedStyles: require_parentheses, omit_parentheses @@ -1255,6 +1257,7 @@ Style/SingleLineBlockParams: - 'app/fever_api/response.rb' # Offense count: 10 +# This cop supports unsafe autocorrection (--autocorrect-all). Style/StaticClass: Exclude: - 'app/commands/feeds/add_new_feed.rb' diff --git a/Gemfile b/Gemfile index a1dce645d..5b2241596 100644 --- a/Gemfile +++ b/Gemfile @@ -2,22 +2,18 @@ ruby_version_file = File.expand_path(".ruby-version", __dir__) ruby File.read(ruby_version_file).chomp if File.readable?(ruby_version_file) source "https://rubygems.org" -gem "activerecord" +gem "rails", "~> 7.0.1" + gem "bcrypt" gem "delayed_job" gem "delayed_job_active_record" gem "feedbag" gem "feedjira" gem "httparty" -gem "i18n" -gem "loofah" -gem "nokogiri" gem "pg" gem "puma", "~> 6.0" -gem "rack-protection" gem "racksh" gem "rack-ssl" -gem "rake" gem "sass" gem "sinatra" gem "sinatra-activerecord" @@ -41,7 +37,6 @@ group :development, :test do gem "coveralls_reborn", require: false gem "faker" gem "pry-byebug" - gem "rack-test" gem "rspec" gem "rspec-html-matchers" gem "shotgun" diff --git a/Gemfile.lock b/Gemfile.lock index 95b6db04c..134b84c26 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,66 @@ GEM remote: https://rubygems.org/ specs: + actioncable (7.0.4) + actionpack (= 7.0.4) + activesupport (= 7.0.4) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (7.0.4) + actionpack (= 7.0.4) + activejob (= 7.0.4) + activerecord (= 7.0.4) + activestorage (= 7.0.4) + activesupport (= 7.0.4) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.0.4) + actionpack (= 7.0.4) + actionview (= 7.0.4) + activejob (= 7.0.4) + activesupport (= 7.0.4) + mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.0) + actionpack (7.0.4) + actionview (= 7.0.4) + activesupport (= 7.0.4) + rack (~> 2.0, >= 2.2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (7.0.4) + actionpack (= 7.0.4) + activerecord (= 7.0.4) + activestorage (= 7.0.4) + activesupport (= 7.0.4) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.0.4) + activesupport (= 7.0.4) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (7.0.4) + activesupport (= 7.0.4) + globalid (>= 0.3.6) activemodel (7.0.4) activesupport (= 7.0.4) activerecord (7.0.4) activemodel (= 7.0.4) activesupport (= 7.0.4) + activestorage (7.0.4) + actionpack (= 7.0.4) + activejob (= 7.0.4) + activerecord (= 7.0.4) + activesupport (= 7.0.4) + marcel (~> 1.0) + mini_mime (>= 1.1.0) activesupport (7.0.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) @@ -15,6 +70,7 @@ GEM public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) bcrypt (3.1.18) + builder (3.2.4) byebug (11.1.3) capybara (3.38.0) addressable @@ -33,6 +89,7 @@ GEM thor (>= 0.20.3, < 2.0) tins (~> 1.16) crass (1.0.6) + date (3.3.2) delayed_job (4.1.11) activesupport (>= 3.0, < 8.0) delayed_job_active_record (4.1.7) @@ -40,6 +97,7 @@ GEM delayed_job (>= 3.0, < 5) diff-lcs (1.5.0) docile (1.4.0) + erubi (1.11.0) execjs (2.8.1) faker (3.0.0) i18n (>= 1.8.11, < 2) @@ -49,6 +107,8 @@ GEM loofah (>= 2.3.1) sax-machine (>= 1.0) ffi (1.15.5) + globalid (1.0.0) + activesupport (>= 5.0) httparty (0.20.0) mime-types (~> 3.0) multi_xml (>= 0.5.2) @@ -58,6 +118,12 @@ GEM loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) + mail (2.8.0) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.2) matrix (0.4.2) method_source (1.0.0) mime-types (3.4.1) @@ -70,6 +136,15 @@ GEM multi_xml (0.6.0) mustermann (3.0.0) ruby2_keywords (~> 0.0.1) + net-imap (0.3.2) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.1) + timeout + net-smtp (0.3.3) + net-protocol nio4r (2.5.8) nokogiri (1.13.10) mini_portile2 (~> 2.8.0) @@ -98,6 +173,32 @@ GEM racksh (1.0.0) rack (>= 1.0) rack-test (>= 0.5) + rails (7.0.4) + actioncable (= 7.0.4) + actionmailbox (= 7.0.4) + actionmailer (= 7.0.4) + actionpack (= 7.0.4) + actiontext (= 7.0.4) + actionview (= 7.0.4) + activejob (= 7.0.4) + activemodel (= 7.0.4) + activerecord (= 7.0.4) + activestorage (= 7.0.4) + activesupport (= 7.0.4) + bundler (>= 1.15.0) + railties (= 7.0.4) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.4.4) + loofah (~> 2.19, >= 2.19.1) + railties (7.0.4) + actionpack (= 7.0.4) + activesupport (= 7.0.4) + method_source + rake (>= 12.2) + thor (~> 1.0) + zeitwerk (~> 2.5) rainbow (3.1.1) rake (13.0.6) rb-fsevent (0.11.2) @@ -185,6 +286,7 @@ GEM thread (0.2.2) tilt (2.0.11) timecop (0.9.6) + timeout (0.3.1) tins (1.32.1) sync tzinfo (2.0.5) @@ -192,15 +294,18 @@ GEM uglifier (4.2.0) execjs (>= 0.3.0, < 3) unicode-display_width (2.3.0) + websocket-driver (0.7.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) will_paginate (3.3.1) xpath (3.2.0) nokogiri (~> 1.8) + zeitwerk (2.6.6) PLATFORMS ruby DEPENDENCIES - activerecord bcrypt capybara coveralls_reborn @@ -210,17 +315,12 @@ DEPENDENCIES feedbag feedjira httparty - i18n - loofah - nokogiri pg pry-byebug puma (~> 6.0) - rack-protection rack-ssl - rack-test racksh - rake + rails (~> 7.0.1) rspec rspec-html-matchers rubocop From e365b4b71bad8ce6badcd4bf73e85ac785add02d Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 17 Dec 2022 16:46:58 -0800 Subject: [PATCH 0390/1107] fix debug page (#694) The mocking hid the fact that the migration methods were no longer available, so I updated the tests to use the actual classes. `ActiveRecord::Base.connection.migration_context.open` always returns a new instance, so for now we continue passing it down in order to avoid needing to use `allow_any_instance_of`. --- app/models/migration_status.rb | 12 ++++-------- spec/models/migration_status_spec.rb | 16 ++++++++-------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/app/models/migration_status.rb b/app/models/migration_status.rb index b89956cbe..6c4d3dc41 100644 --- a/app/models/migration_status.rb +++ b/app/models/migration_status.rb @@ -1,17 +1,13 @@ class MigrationStatus attr_reader :migrator - def initialize(migrator = ActiveRecord::Migrator) + def initialize(migrator = ActiveRecord::Base.connection.migration_context.open) @migrator = migrator end def pending_migrations - migrations_path = migrator.migrations_path - migrations = migrator.migrations(migrations_path) - current_version = migrator.current_version - - migrations - .select { |m| current_version < m.version } - .map { |m| "#{m.name} - #{m.version}" } + migrator.pending_migrations.map do |migration| + "#{migration.name} - #{migration.version}" + end end end diff --git a/spec/models/migration_status_spec.rb b/spec/models/migration_status_spec.rb index bb3791d5d..d32f79683 100644 --- a/spec/models/migration_status_spec.rb +++ b/spec/models/migration_status_spec.rb @@ -6,14 +6,14 @@ describe "MigrationStatus" do describe "pending_migrations" do it "returns array of strings representing pending migrations" do - migrator = double "Migrator" - allow(migrator).to receive(:migrations).and_return [ - double("First Migration", name: "Migration A", version: 1), - double("Second Migration", name: "Migration B", version: 2), - double("Third Migration", name: "Migration C", version: 3) - ] - allow(migrator).to receive(:migrations_path) - allow(migrator).to receive(:current_version).and_return 1 + migrator = ActiveRecord::Base.connection.migration_context.open + + allow(migrator).to receive(:pending_migrations).and_return( + [ + ActiveRecord::Migration.new("Migration B", 2), + ActiveRecord::Migration.new("Migration C", 3) + ] + ) expect(MigrationStatus.new(migrator).pending_migrations) .to eq(["Migration B - 2", "Migration C - 3"]) From 1c366cacac599875625cec2bbd57539efbc16a58 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 17 Dec 2022 22:57:08 -0800 Subject: [PATCH 0391/1107] RuboCop: disable ConstantResolution linter (#695) --- .rubocop.yml | 1 + .rubocop_todo.yml | 156 +--------------------------------------------- 2 files changed, 4 insertions(+), 153 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index d339f6614..8c8b74124 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -51,5 +51,6 @@ Style/StringLiterals: # ################################################################################ +Lint/ConstantResolution: { Enabled: false } Rails/SchemaComment: { Enabled: false } Style/RequireOrder: { Enabled: false } diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c32b4ef89..1af91c2cd 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 400` -# on 2022-12-17 23:28:03 UTC using RuboCop version 1.40.0. +# on 2022-12-18 06:47:47 UTC using RuboCop version 1.40.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -53,155 +53,6 @@ Lint/AmbiguousOperatorPrecedence: - 'spec/factories/feed_factory.rb' - 'spec/factories/group_factory.rb' -# Offense count: 776 -# Configuration parameters: Only, Ignore. -Lint/ConstantResolution: - Exclude: - - 'Gemfile' - - 'Rakefile' - - 'app.rb' - - 'app/commands/feeds/add_new_feed.rb' - - 'app/commands/feeds/export_to_opml.rb' - - 'app/commands/feeds/find_new_stories.rb' - - 'app/commands/feeds/import_from_opml.rb' - - 'app/commands/stories/mark_all_as_read.rb' - - 'app/commands/stories/mark_as_read.rb' - - 'app/commands/stories/mark_as_starred.rb' - - 'app/commands/stories/mark_as_unread.rb' - - 'app/commands/stories/mark_as_unstarred.rb' - - 'app/commands/stories/mark_feed_as_read.rb' - - 'app/commands/stories/mark_group_as_read.rb' - - 'app/commands/users/change_user_password.rb' - - 'app/commands/users/create_user.rb' - - 'app/commands/users/sign_in_user.rb' - - 'app/controllers/debug_controller.rb' - - 'app/controllers/feeds_controller.rb' - - 'app/controllers/first_run_controller.rb' - - 'app/controllers/sessions_controller.rb' - - 'app/controllers/stories_controller.rb' - - 'app/fever_api/authentication.rb' - - 'app/fever_api/read_feeds.rb' - - 'app/fever_api/read_feeds_groups.rb' - - 'app/fever_api/read_groups.rb' - - 'app/fever_api/read_items.rb' - - 'app/fever_api/response.rb' - - 'app/fever_api/sync_saved_item_ids.rb' - - 'app/fever_api/sync_unread_item_ids.rb' - - 'app/fever_api/write_mark_feed.rb' - - 'app/fever_api/write_mark_group.rb' - - 'app/fever_api/write_mark_item.rb' - - 'app/helpers/authentication_helpers.rb' - - 'app/helpers/url_helpers.rb' - - 'app/jobs/fetch_feed_job.rb' - - 'app/models/application_record.rb' - - 'app/models/migration_status.rb' - - 'app/models/story.rb' - - 'app/repositories/feed_repository.rb' - - 'app/repositories/group_repository.rb' - - 'app/repositories/story_repository.rb' - - 'app/repositories/user_repository.rb' - - 'app/tasks/change_password.rb' - - 'app/tasks/fetch_feed.rb' - - 'app/tasks/fetch_feeds.rb' - - 'app/tasks/remove_old_stories.rb' - - 'app/utils/api_key.rb' - - 'app/utils/content_sanitizer.rb' - - 'app/utils/feed_discovery.rb' - - 'app/utils/opml_parser.rb' - - 'app/utils/sample_story.rb' - - 'config.ru' - - 'config/asset_pipeline.rb' - - 'config/puma.rb' - - 'db/migrate/20130409010818_create_feeds.rb' - - 'db/migrate/20130409010826_create_stories.rb' - - 'db/migrate/20130412185253_add_new_fields_to_stories.rb' - - 'db/migrate/20130418221144_add_user_model.rb' - - 'db/migrate/20130423001740_drop_email_from_user.rb' - - 'db/migrate/20130423180446_remove_author_from_stories.rb' - - 'db/migrate/20130425211008_add_setup_complete_to_user.rb' - - 'db/migrate/20130425222157_add_delayed_job.rb' - - 'db/migrate/20130429232127_add_status_to_feeds.rb' - - 'db/migrate/20130504005816_text_url.rb' - - 'db/migrate/20130504022615_change_story_permalink_column.rb' - - 'db/migrate/20130509131045_add_unique_constraints.rb' - - 'db/migrate/20130513025939_add_keep_unread_to_stories.rb' - - 'db/migrate/20130513044029_add_is_starred_status_for_stories.rb' - - 'db/migrate/20130522014405_add_api_key_to_user.rb' - - 'db/migrate/20130730120312_add_entry_id_to_stories.rb' - - 'db/migrate/20130805113712_update_stories_unique_constraints.rb' - - 'db/migrate/20130821020313_update_nil_entry_ids.rb' - - 'db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb' - - 'db/migrate/20140413100725_add_groups_table_and_foreign_keys_to_feeds.rb' - - 'db/migrate/20140421224454_fix_invalid_unicode.rb' - - 'db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb' - - 'db/migrate/20221206231914_add_enclosure_url_to_stories.rb' - - 'fever_api.rb' - - 'spec/commands/feeds/add_new_feed_spec.rb' - - 'spec/commands/feeds/export_to_opml_spec.rb' - - 'spec/commands/feeds/import_from_opml_spec.rb' - - 'spec/commands/find_new_stories_spec.rb' - - 'spec/commands/stories/mark_all_as_read_spec.rb' - - 'spec/commands/stories/mark_as_read_spec.rb' - - 'spec/commands/stories/mark_as_starred_spec.rb' - - 'spec/commands/stories/mark_as_unread_spec.rb' - - 'spec/commands/stories/mark_as_unstarred_spec.rb' - - 'spec/commands/stories/mark_feed_as_read_spec.rb' - - 'spec/commands/stories/mark_group_as_read_spec.rb' - - 'spec/commands/users/change_user_password_spec.rb' - - 'spec/commands/users/complete_setup_spec.rb' - - 'spec/commands/users/create_user_spec.rb' - - 'spec/commands/users/sign_in_user_spec.rb' - - 'spec/controllers/debug_controller_spec.rb' - - 'spec/controllers/feeds_controller_spec.rb' - - 'spec/controllers/first_run_controller_spec.rb' - - 'spec/controllers/sessions_controller_spec.rb' - - 'spec/controllers/stories_controller_spec.rb' - - 'spec/factories/feed_factory.rb' - - 'spec/factories/feeds.rb' - - 'spec/factories/group_factory.rb' - - 'spec/factories/groups.rb' - - 'spec/factories/stories.rb' - - 'spec/factories/story_factory.rb' - - 'spec/factories/user_factory.rb' - - 'spec/factories/users.rb' - - 'spec/fever_api/authentication_spec.rb' - - 'spec/fever_api/read_favicons_spec.rb' - - 'spec/fever_api/read_feeds_groups_spec.rb' - - 'spec/fever_api/read_feeds_spec.rb' - - 'spec/fever_api/read_groups_spec.rb' - - 'spec/fever_api/read_items_spec.rb' - - 'spec/fever_api/read_links_spec.rb' - - 'spec/fever_api/sync_saved_item_ids_spec.rb' - - 'spec/fever_api/sync_unread_item_ids_spec.rb' - - 'spec/fever_api/write_mark_feed_spec.rb' - - 'spec/fever_api/write_mark_group_spec.rb' - - 'spec/fever_api/write_mark_item_spec.rb' - - 'spec/fever_api_spec.rb' - - 'spec/helpers/authentications_helper_spec.rb' - - 'spec/helpers/url_helpers_spec.rb' - - 'spec/integration/feed_importing_spec.rb' - - 'spec/javascript/test_controller.rb' - - 'spec/models/feed_spec.rb' - - 'spec/models/group_spec.rb' - - 'spec/models/migration_status_spec.rb' - - 'spec/models/story_spec.rb' - - 'spec/repositories/feed_repository_spec.rb' - - 'spec/repositories/group_repository_spec.rb' - - 'spec/repositories/story_repository_spec.rb' - - 'spec/repositories/user_repository_spec.rb' - - 'spec/spec_helper.rb' - - 'spec/support/active_record.rb' - - 'spec/support/coverage.rb' - - 'spec/support/feed_server.rb' - - 'spec/tasks/change_password_spec.rb' - - 'spec/tasks/fetch_feed_spec.rb' - - 'spec/tasks/fetch_feeds_spec.rb' - - 'spec/tasks/remove_old_stories_spec.rb' - - 'spec/utils/content_sanitizer_spec.rb' - - 'spec/utils/feed_discovery_spec.rb' - - 'spec/utils/i18n_support_spec.rb' - - 'spec/utils/opml_parser_spec.rb' - # Offense count: 1 # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: @@ -674,7 +525,7 @@ RSpec/StubbedMock: - 'spec/tasks/remove_old_stories_spec.rb' - 'spec/utils/feed_discovery_spec.rb' -# Offense count: 96 +# Offense count: 92 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Exclude: @@ -698,7 +549,6 @@ RSpec/VerifiedDoubles: - 'spec/fever_api/write_mark_group_spec.rb' - 'spec/fever_api/write_mark_item_spec.rb' - 'spec/fever_api_spec.rb' - - 'spec/models/migration_status_spec.rb' - 'spec/repositories/story_repository_spec.rb' - 'spec/tasks/fetch_feed_spec.rb' - 'spec/tasks/remove_old_stories_spec.rb' @@ -1082,7 +932,7 @@ Style/InlineComment: Exclude: - 'app/utils/opml_parser.rb' -# Offense count: 699 +# Offense count: 694 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: IgnoreMacros, AllowedMethods, IgnoredMethods, AllowedPatterns, IgnoredPatterns, IncludedMacros, AllowParenthesesInMultilineCall, AllowParenthesesInChaining, AllowParenthesesInCamelCaseMethod, AllowParenthesesInStringInterpolation, EnforcedStyle. # SupportedStyles: require_parentheses, omit_parentheses From f8ccc6b82a29b5ed09c95c03fdd6f5d512e1486b Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 09:41:44 -0800 Subject: [PATCH 0392/1107] Rails: switch DebugController to use ActionController (#696) This introduces a new helper method that allows us to render views via `ActionController` rather than `Sinatra::Base`. This will help us more incrementally move the app to Rails. --- .rubocop_todo.yml | 2 + Gemfile | 1 + Gemfile.lock | 9 ++++ app.rb | 13 ++++- app/controllers/application_controller.rb | 18 +++++++ app/controllers/debug_controller.rb | 19 ++++--- app/helpers/controller_helpers.rb | 17 +++++++ .../{heroku.erb => debug/heroku.html.erb} | 0 app/views/{debug.erb => debug/index.html.erb} | 0 app/views/layouts/_flash.html.erb | 29 +++++++++++ app/views/layouts/_footer.html.erb | 25 ++++++++++ app/views/layouts/_shortcuts.html.erb | 24 +++++++++ app/views/layouts/application.html.erb | 49 +++++++++++++++++++ config/asset_pipeline.rb | 8 +-- spec/controllers/debug_controller_spec.rb | 7 ++- 15 files changed, 206 insertions(+), 15 deletions(-) create mode 100644 app/controllers/application_controller.rb create mode 100644 app/helpers/controller_helpers.rb rename app/views/{heroku.erb => debug/heroku.html.erb} (100%) rename app/views/{debug.erb => debug/index.html.erb} (100%) create mode 100644 app/views/layouts/_flash.html.erb create mode 100644 app/views/layouts/_footer.html.erb create mode 100644 app/views/layouts/_shortcuts.html.erb create mode 100644 app/views/layouts/application.html.erb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 1af91c2cd..13212ec00 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -141,6 +141,7 @@ RSpec/AlignRightLetBrace: # Offense count: 6 RSpec/AnyInstance: Exclude: + - 'spec/controllers/debug_controller_spec.rb' - 'spec/controllers/feeds_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' @@ -1125,6 +1126,7 @@ Style/StaticClass: # This cop supports unsafe autocorrection (--autocorrect-all). Style/StringHashKeys: Exclude: + - 'app/helpers/controller_helpers.rb' - 'fever_api.rb' - 'spec/app_spec.rb' - 'spec/controllers/debug_controller_spec.rb' diff --git a/Gemfile b/Gemfile index 5b2241596..0574bd935 100644 --- a/Gemfile +++ b/Gemfile @@ -39,6 +39,7 @@ group :development, :test do gem "pry-byebug" gem "rspec" gem "rspec-html-matchers" + gem "rspec-rails" gem "shotgun" gem "simplecov" gem "timecop" diff --git a/Gemfile.lock b/Gemfile.lock index 134b84c26..e96b88b78 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -221,6 +221,14 @@ GEM rspec-mocks (3.12.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) + rspec-rails (6.0.1) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.11) + rspec-expectations (~> 3.11) + rspec-mocks (~> 3.11) + rspec-support (~> 3.11) rspec-support (3.12.0) rubocop (1.40.0) json (~> 2.3) @@ -323,6 +331,7 @@ DEPENDENCIES rails (~> 7.0.1) rspec rspec-html-matchers + rspec-rails rubocop rubocop-rails rubocop-rake diff --git a/app.rb b/app.rb index 80a22b60b..2e47ab51a 100644 --- a/app.rb +++ b/app.rb @@ -1,3 +1,6 @@ +require "action_pack" +require "action_view" +require "action_controller" require "sinatra/base" require "sinatra/activerecord" require "sinatra/flash" @@ -12,9 +15,13 @@ require "securerandom" require_relative "app/helpers/authentication_helpers" +require_relative "app/helpers/controller_helpers" require_relative "app/repositories/user_repository" require_relative "config/asset_pipeline" +require_relative "app/controllers/application_controller" +require_relative "app/controllers/debug_controller" + I18n.load_path += Dir[File.join(File.dirname(__FILE__), "config/locales", "*.yml").to_s] I18n.config.enforce_available_locales = false Time.zone = ENV.fetch("TZ", "UTC") @@ -25,6 +32,8 @@ class Stringer < Sinatra::Base use Rack::SSL, exclude: ->(env) { env["PATH_INFO"] =~ %r{^/(js|css|img)} } end + extend Sinatra::ControllerHelpers + register Sinatra::ActiveRecordExtension register Sinatra::Flash register Sinatra::Contrib @@ -80,10 +89,12 @@ def t(*args, **kwargs) redirect to("/setup/password") end end + + rails_route(:get, "/debug", to: "debug#index") + rails_route(:get, "/heroku", to: "debug#heroku") end require_relative "app/controllers/stories_controller" require_relative "app/controllers/first_run_controller" require_relative "app/controllers/sessions_controller" require_relative "app/controllers/feeds_controller" -require_relative "app/controllers/debug_controller" diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 000000000..b0599055d --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class ApplicationController < ActionController::Base + include Sinatra::AuthenticationHelpers + helper_method :current_user + + before_action :append_view_path + + # needed for Sinatra + def append_view_path + super("./app/views") + end + + def flash + session["flash"] + end + helper_method :flash +end diff --git a/app/controllers/debug_controller.rb b/app/controllers/debug_controller.rb index 018a29d4d..feece9b69 100644 --- a/app/controllers/debug_controller.rb +++ b/app/controllers/debug_controller.rb @@ -1,15 +1,14 @@ require_relative "../models/migration_status" -class Stringer < Sinatra::Base - get "/debug" do - erb :debug, - locals: { - queued_jobs_count: Delayed::Job.count, - pending_migrations: MigrationStatus.new.pending_migrations - } +class DebugController < ApplicationController + def index + render( + locals: { + queued_jobs_count: Delayed::Job.count, + pending_migrations: MigrationStatus.new.pending_migrations + } + ) end - get "/heroku" do - erb :heroku - end + def heroku; end end diff --git a/app/helpers/controller_helpers.rb b/app/helpers/controller_helpers.rb new file mode 100644 index 000000000..efee39f79 --- /dev/null +++ b/app/helpers/controller_helpers.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Sinatra + module ControllerHelpers + def rails_route(method, path, options) + options = options.with_indifferent_access + to = options.delete(:to) + controller_name, action_name = to.split("#") + controller_klass = "#{controller_name.camelize}Controller".constantize + route(method.to_s.upcase, path, options) do + # Make sure that our parsed URL params are where Rack (and ActionDispatch) expect them + app = controller_klass.action(action_name) + app.call(request.env.merge("rack.request.query_hash" => params)) + end + end + end +end diff --git a/app/views/heroku.erb b/app/views/debug/heroku.html.erb similarity index 100% rename from app/views/heroku.erb rename to app/views/debug/heroku.html.erb diff --git a/app/views/debug.erb b/app/views/debug/index.html.erb similarity index 100% rename from app/views/debug.erb rename to app/views/debug/index.html.erb diff --git a/app/views/layouts/_flash.html.erb b/app/views/layouts/_flash.html.erb new file mode 100644 index 000000000..2155dc810 --- /dev/null +++ b/app/views/layouts/_flash.html.erb @@ -0,0 +1,29 @@ +<% if flash.has_key? :success %> +
    + <%= flash[:success] %> +
    +<% end %> + +<% if flash.has_key? :error %> +
    + <%= flash[:error] %> +
    +<% end %> + + + + + + \ No newline at end of file diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb new file mode 100644 index 000000000..42d74a31e --- /dev/null +++ b/app/views/layouts/_footer.html.erb @@ -0,0 +1,25 @@ +
    +
    +
    + +
    +
    +

    + <%= t('layout.hey') %> <%= t('layout.back_to_work') %> +

    +
    +
    +
    diff --git a/app/views/layouts/_shortcuts.html.erb b/app/views/layouts/_shortcuts.html.erb new file mode 100644 index 000000000..94e255753 --- /dev/null +++ b/app/views/layouts/_shortcuts.html.erb @@ -0,0 +1,24 @@ + diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 000000000..e04bd2698 --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,49 @@ + + + + + <%= content_for(:title) %> + <%= t('layout.title') %> + + + + + + + + + + + + <%= content_for(:head) %> + + + + + +
    +
    + <%= render 'layouts/flash' %> + <%= render 'layouts/shortcuts' if current_user %> +
    +
    +
    + <%= yield %> +
    +
    +
    +
    + +
    +
    + + + + + + diff --git a/config/asset_pipeline.rb b/config/asset_pipeline.rb index 68fc62ed1..914008343 100644 --- a/config/asset_pipeline.rb +++ b/config/asset_pipeline.rb @@ -2,9 +2,11 @@ module AssetPipeline def registered(app) app.set :sprockets, Sprockets::Environment.new(app.root) - app.get "/assets/*" do - env["PATH_INFO"].sub!(%r{^/assets}, "") - settings.sprockets.call(env) + %w[assets stylesheets javascripts].each do |path| + app.get "/#{path}/*" do + env["PATH_INFO"].sub!(%r{^/#{path}}, "") + settings.sprockets.call(env) + end end append_paths(app) diff --git a/spec/controllers/debug_controller_spec.rb b/spec/controllers/debug_controller_spec.rb index 864ae989c..63a3825e3 100644 --- a/spec/controllers/debug_controller_spec.rb +++ b/spec/controllers/debug_controller_spec.rb @@ -3,7 +3,12 @@ app_require "controllers/debug_controller" -describe "DebugController" do +describe DebugController do + before do + # for Sinatra + allow_any_instance_of(described_class).to receive(:session).and_return({ "flash" => {} }) + end + describe "GET /debug" do before do delayed_job = double "Delayed::Job" From 9975a1322426610a40dca4a83cebcc950e6d5437 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 10:09:27 -0800 Subject: [PATCH 0393/1107] Rails: relocate sinatra controllers (#697) This will allow us to gradually port actions to ActionController. Leaving the controller specs where they are, as they should largely work the same for both. --- .rubocop_todo.yml | 30 +++++++++---------- app.rb | 8 ++--- .../{ => sinatra}/feeds_controller.rb | 6 ++-- .../{ => sinatra}/first_run_controller.rb | 12 ++++---- .../{ => sinatra}/sessions_controller.rb | 2 +- .../{ => sinatra}/stories_controller.rb | 4 +-- spec/controllers/feeds_controller_spec.rb | 2 +- spec/controllers/first_run_controller_spec.rb | 2 +- spec/controllers/sessions_controller_spec.rb | 2 +- spec/controllers/stories_controller_spec.rb | 2 +- 10 files changed, 35 insertions(+), 35 deletions(-) rename app/controllers/{ => sinatra}/feeds_controller.rb (89%) rename app/controllers/{ => sinatra}/first_run_controller.rb (74%) rename app/controllers/{ => sinatra}/sessions_controller.rb (91%) rename app/controllers/{ => sinatra}/stories_controller.rb (89%) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 13212ec00..214123d58 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,19 +1,19 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 400` -# on 2022-12-18 06:47:47 UTC using RuboCop version 1.40.0. +# on 2022-12-18 18:04:01 UTC using RuboCop version 1.40.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 34 +# Offense count: 35 # Configuration parameters: Include, IgnoredGems, OnlyFor. # Include: **/*.gemfile, **/Gemfile, **/gems.rb Bundler/GemComment: Exclude: - 'Gemfile' -# Offense count: 32 +# Offense count: 33 # Configuration parameters: EnforcedStyle, Include, AllowedGems. # SupportedStyles: required, forbidden # Include: **/*.gemfile, **/Gemfile, **/gems.rb @@ -138,7 +138,7 @@ RSpec/AlignRightLetBrace: - 'spec/tasks/fetch_feeds_spec.rb' - 'spec/utils/feed_discovery_spec.rb' -# Offense count: 6 +# Offense count: 7 RSpec/AnyInstance: Exclude: - 'spec/controllers/debug_controller_spec.rb' @@ -638,8 +638,8 @@ Rails/SaveBang: - 'app/commands/users/change_user_password.rb' - 'app/commands/users/complete_setup.rb' - 'app/commands/users/create_user.rb' - - 'app/controllers/first_run_controller.rb' - - 'app/controllers/stories_controller.rb' + - 'app/controllers/sinatra/first_run_controller.rb' + - 'app/controllers/sinatra/stories_controller.rb' - 'app/repositories/feed_repository.rb' - 'app/repositories/story_repository.rb' - 'app/repositories/user_repository.rb' @@ -706,7 +706,7 @@ Rails/WhereNot: # MethodsAcceptingSymbol: inject, reduce Style/CollectionMethods: Exclude: - - 'app/controllers/stories_controller.rb' + - 'app/controllers/sinatra/stories_controller.rb' - 'app/fever_api/response.rb' # Offense count: 8 @@ -753,10 +753,10 @@ Style/FrozenStringLiteralComment: - 'app/commands/users/create_user.rb' - 'app/commands/users/sign_in_user.rb' - 'app/controllers/debug_controller.rb' - - 'app/controllers/feeds_controller.rb' - - 'app/controllers/first_run_controller.rb' - - 'app/controllers/sessions_controller.rb' - - 'app/controllers/stories_controller.rb' + - 'app/controllers/sinatra/feeds_controller.rb' + - 'app/controllers/sinatra/first_run_controller.rb' + - 'app/controllers/sinatra/sessions_controller.rb' + - 'app/controllers/sinatra/stories_controller.rb' - 'app/fever_api/authentication.rb' - 'app/fever_api/read_favicons.rb' - 'app/fever_api/read_feeds.rb' @@ -933,7 +933,7 @@ Style/InlineComment: Exclude: - 'app/utils/opml_parser.rb' -# Offense count: 694 +# Offense count: 695 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: IgnoreMacros, AllowedMethods, IgnoredMethods, AllowedPatterns, IgnoredPatterns, IncludedMacros, AllowParenthesesInMultilineCall, AllowParenthesesInChaining, AllowParenthesesInCamelCaseMethod, AllowParenthesesInStringInterpolation, EnforcedStyle. # SupportedStyles: require_parentheses, omit_parentheses @@ -1081,7 +1081,7 @@ Style/PercentLiteralDelimiters: # SupportedStyles: same_as_string_literals, single_quotes, double_quotes Style/QuotedSymbols: Exclude: - - 'app/controllers/feeds_controller.rb' + - 'app/controllers/sinatra/feeds_controller.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). @@ -1097,7 +1097,7 @@ Style/ReturnNil: # AllowedMethods: present?, blank?, presence, try, try! Style/SafeNavigation: Exclude: - - 'app/controllers/feeds_controller.rb' + - 'app/controllers/sinatra/feeds_controller.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). @@ -1122,7 +1122,7 @@ Style/StaticClass: - 'app/utils/api_key.rb' - 'app/utils/content_sanitizer.rb' -# Offense count: 18 +# Offense count: 20 # This cop supports unsafe autocorrection (--autocorrect-all). Style/StringHashKeys: Exclude: diff --git a/app.rb b/app.rb index 2e47ab51a..f32f75c93 100644 --- a/app.rb +++ b/app.rb @@ -94,7 +94,7 @@ def t(*args, **kwargs) rails_route(:get, "/heroku", to: "debug#heroku") end -require_relative "app/controllers/stories_controller" -require_relative "app/controllers/first_run_controller" -require_relative "app/controllers/sessions_controller" -require_relative "app/controllers/feeds_controller" +require_relative "app/controllers/sinatra/stories_controller" +require_relative "app/controllers/sinatra/first_run_controller" +require_relative "app/controllers/sinatra/sessions_controller" +require_relative "app/controllers/sinatra/feeds_controller" diff --git a/app/controllers/feeds_controller.rb b/app/controllers/sinatra/feeds_controller.rb similarity index 89% rename from app/controllers/feeds_controller.rb rename to app/controllers/sinatra/feeds_controller.rb index 0cbb0edb7..ecf7da92e 100644 --- a/app/controllers/feeds_controller.rb +++ b/app/controllers/sinatra/feeds_controller.rb @@ -1,6 +1,6 @@ -require_relative "../repositories/feed_repository" -require_relative "../commands/feeds/add_new_feed" -require_relative "../commands/feeds/export_to_opml" +require_relative "../../repositories/feed_repository" +require_relative "../../commands/feeds/add_new_feed" +require_relative "../../commands/feeds/export_to_opml" class Stringer < Sinatra::Base get "/feeds" do diff --git a/app/controllers/first_run_controller.rb b/app/controllers/sinatra/first_run_controller.rb similarity index 74% rename from app/controllers/first_run_controller.rb rename to app/controllers/sinatra/first_run_controller.rb index 6be12f769..79eb8f58d 100644 --- a/app/controllers/first_run_controller.rb +++ b/app/controllers/sinatra/first_run_controller.rb @@ -1,9 +1,9 @@ -require_relative "../commands/feeds/import_from_opml" -require_relative "../commands/users/create_user" -require_relative "../commands/users/complete_setup" -require_relative "../repositories/user_repository" -require_relative "../repositories/story_repository" -require_relative "../tasks/fetch_feeds" +require_relative "../../commands/feeds/import_from_opml" +require_relative "../../commands/users/create_user" +require_relative "../../commands/users/complete_setup" +require_relative "../../repositories/user_repository" +require_relative "../../repositories/story_repository" +require_relative "../../tasks/fetch_feeds" class Stringer < Sinatra::Base namespace "/setup" do diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sinatra/sessions_controller.rb similarity index 91% rename from app/controllers/sessions_controller.rb rename to app/controllers/sinatra/sessions_controller.rb index 54d40df42..bd305b390 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sinatra/sessions_controller.rb @@ -1,4 +1,4 @@ -require_relative "../commands/users/sign_in_user" +require_relative "../../commands/users/sign_in_user" class Stringer < Sinatra::Base get "/login" do diff --git a/app/controllers/stories_controller.rb b/app/controllers/sinatra/stories_controller.rb similarity index 89% rename from app/controllers/stories_controller.rb rename to app/controllers/sinatra/stories_controller.rb index 052682046..bf9cb3049 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/sinatra/stories_controller.rb @@ -1,5 +1,5 @@ -require_relative "../repositories/story_repository" -require_relative "../commands/stories/mark_all_as_read" +require_relative "../../repositories/story_repository" +require_relative "../../commands/stories/mark_all_as_read" class Stringer < Sinatra::Base get "/news" do diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index 3f61f6efa..22e843dc7 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -app_require "controllers/feeds_controller" +app_require "controllers/sinatra/feeds_controller" describe "FeedsController" do let(:feeds) { [FeedFactory.build, FeedFactory.build] } diff --git a/spec/controllers/first_run_controller_spec.rb b/spec/controllers/first_run_controller_spec.rb index b9ec24a93..192a8e1b2 100644 --- a/spec/controllers/first_run_controller_spec.rb +++ b/spec/controllers/first_run_controller_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" require "support/active_record" -app_require "controllers/first_run_controller" +app_require "controllers/sinatra/first_run_controller" describe "FirstRunController" do context "when a user has not been setup" do diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 87ef61b18..85eda0ade 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -app_require "controllers/sessions_controller" +app_require "controllers/sinatra/sessions_controller" describe "SessionsController" do describe "GET /login" do diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index 234dcc024..f1a4ea52e 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" require "will_paginate/array" -app_require "controllers/stories_controller" +app_require "controllers/sinatra/stories_controller" describe "StoriesController" do let(:story_one) { StoryFactory.build } From 715796355b77d5f3f9f828faec58baa5471b9385 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 10:27:22 -0800 Subject: [PATCH 0394/1107] Rails: switch feeds#index to ActionController (#698) --- app.rb | 2 ++ app/controllers/feeds_controller.rb | 7 ++++++ app/controllers/sinatra/feeds_controller.rb | 6 ----- app/views/feeds/_action_bar.html.erb | 22 +++++++++++++++++++ app/views/{partials => feeds}/_feed.erb | 0 app/views/feeds/{index.erb => index.html.erb} | 6 ++--- spec/controllers/feeds_controller_spec.rb | 5 +++++ 7 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 app/controllers/feeds_controller.rb create mode 100644 app/views/feeds/_action_bar.html.erb rename app/views/{partials => feeds}/_feed.erb (100%) rename app/views/feeds/{index.erb => index.html.erb} (81%) diff --git a/app.rb b/app.rb index f32f75c93..80ab35d74 100644 --- a/app.rb +++ b/app.rb @@ -21,6 +21,7 @@ require_relative "app/controllers/application_controller" require_relative "app/controllers/debug_controller" +require_relative "app/controllers/feeds_controller" I18n.load_path += Dir[File.join(File.dirname(__FILE__), "config/locales", "*.yml").to_s] I18n.config.enforce_available_locales = false @@ -92,6 +93,7 @@ def t(*args, **kwargs) rails_route(:get, "/debug", to: "debug#index") rails_route(:get, "/heroku", to: "debug#heroku") + rails_route(:get, "/feeds", to: "feeds#index") end require_relative "app/controllers/sinatra/stories_controller" diff --git a/app/controllers/feeds_controller.rb b/app/controllers/feeds_controller.rb new file mode 100644 index 000000000..3f6ae66a2 --- /dev/null +++ b/app/controllers/feeds_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class FeedsController < ApplicationController + def index + @feeds = FeedRepository.list + end +end diff --git a/app/controllers/sinatra/feeds_controller.rb b/app/controllers/sinatra/feeds_controller.rb index ecf7da92e..8fd63fd36 100644 --- a/app/controllers/sinatra/feeds_controller.rb +++ b/app/controllers/sinatra/feeds_controller.rb @@ -3,12 +3,6 @@ require_relative "../../commands/feeds/export_to_opml" class Stringer < Sinatra::Base - get "/feeds" do - @feeds = FeedRepository.list - - erb :'feeds/index' - end - get "/feeds/:id/edit" do @feed = FeedRepository.fetch(params[:id]) diff --git a/app/views/feeds/_action_bar.html.erb b/app/views/feeds/_action_bar.html.erb new file mode 100644 index 000000000..f11a3d0c4 --- /dev/null +++ b/app/views/feeds/_action_bar.html.erb @@ -0,0 +1,22 @@ +
    +
    + + + +
    + + +
    diff --git a/app/views/partials/_feed.erb b/app/views/feeds/_feed.erb similarity index 100% rename from app/views/partials/_feed.erb rename to app/views/feeds/_feed.erb diff --git a/app/views/feeds/index.erb b/app/views/feeds/index.html.erb similarity index 81% rename from app/views/feeds/index.erb rename to app/views/feeds/index.html.erb index 0dad10031..7709e4633 100644 --- a/app/views/feeds/index.erb +++ b/app/views/feeds/index.html.erb @@ -1,12 +1,12 @@
    - <%= render_partial :feed_action_bar %> + <%= render "feeds/action_bar" %>
    <% unless @feeds.empty? %>
      <% @feeds.each do |feed| %> - <%= render_partial :feed, { feed: feed } %> + <%= render "feeds/feed", { feed: feed } %> <% end %>
    @@ -20,4 +20,4 @@ $(document).ready(function () { $(".status").tooltip(); }); - \ No newline at end of file + diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index 22e843dc7..c986e6dd1 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -5,6 +5,11 @@ describe "FeedsController" do let(:feeds) { [FeedFactory.build, FeedFactory.build] } + before do + # for Sinatra + allow_any_instance_of(FeedsController).to receive(:session).and_return({ "flash" => {} }) + end + describe "GET /feeds" do it "renders a list of feeds" do expect(FeedRepository).to receive(:list).and_return(feeds) From af417bc56aa687f9d5dfc9fdf0e07f694cc4bdef Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 10:37:10 -0800 Subject: [PATCH 0395/1107] Rails: fix issue with Delayed::Job (#699) Now that we're loading `ActionController` it's also loading `ActionDispatch`, causing the second case in [this line][tl] to raise an error. For now we'll stub out `Rails` to prevent it. [tl]: https://github.com/collectiveidea/delayed_job/blob/5ac5adea8d18325d0470eeebfa81227b1f5961e3/lib/delayed/worker.rb#L119 --- .rubocop_todo.yml | 1 + app.rb | 6 ++++++ spec/app_spec.rb | 6 ++++++ 3 files changed, 13 insertions(+) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 214123d58..32562e00f 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1034,6 +1034,7 @@ Style/NumericPredicate: # Offense count: 4 Style/OpenStructUse: Exclude: + - 'app.rb' - 'spec/factories/feed_factory.rb' - 'spec/factories/group_factory.rb' - 'spec/factories/story_factory.rb' diff --git a/app.rb b/app.rb index 80ab35d74..593e90d5c 100644 --- a/app.rb +++ b/app.rb @@ -23,6 +23,12 @@ require_relative "app/controllers/debug_controller" require_relative "app/controllers/feeds_controller" +module Rails + def self.application + OpenStruct.new(config: OpenStruct.new(cache_classes: true)) + end +end + I18n.load_path += Dir[File.join(File.dirname(__FILE__), "config/locales", "*.yml").to_s] I18n.config.enforce_available_locales = false Time.zone = ENV.fetch("TZ", "UTC") diff --git a/spec/app_spec.rb b/spec/app_spec.rb index b8faeebf9..bdffb08d3 100644 --- a/spec/app_spec.rb +++ b/spec/app_spec.rb @@ -2,6 +2,12 @@ require "support/active_record" describe "App" do + describe "Rails" do + it "returns a fake application" do + expect(Rails.application.config.cache_classes).to be(true) + end + end + context "when user is not authenticated and page requires authentication" do it "sets the session redirect_to" do create_user(:setup_complete) From 69093cf8af04ab50b25f2f25ef88baa4a8c5bccc Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 11:08:29 -0800 Subject: [PATCH 0396/1107] Rails: convert Feeds edit/update to ActionController (#700) --- app.rb | 2 ++ app/controllers/feeds_controller.rb | 13 +++++++++++++ app/controllers/sinatra/feeds_controller.rb | 15 --------------- app/views/feeds/{_feed.erb => _feed.html.erb} | 0 app/views/feeds/{edit.erb => edit.html.erb} | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) rename app/views/feeds/{_feed.erb => _feed.html.erb} (100%) rename app/views/feeds/{edit.erb => edit.html.erb} (97%) diff --git a/app.rb b/app.rb index 593e90d5c..d4671f1b7 100644 --- a/app.rb +++ b/app.rb @@ -100,6 +100,8 @@ def t(*args, **kwargs) rails_route(:get, "/debug", to: "debug#index") rails_route(:get, "/heroku", to: "debug#heroku") rails_route(:get, "/feeds", to: "feeds#index") + rails_route(:get, "/feeds/:id/edit", to: "feeds#edit") + rails_route(:put, "/feeds/:id", to: "feeds#update") end require_relative "app/controllers/sinatra/stories_controller" diff --git a/app/controllers/feeds_controller.rb b/app/controllers/feeds_controller.rb index 3f6ae66a2..7cef7d6d6 100644 --- a/app/controllers/feeds_controller.rb +++ b/app/controllers/feeds_controller.rb @@ -4,4 +4,17 @@ class FeedsController < ApplicationController def index @feeds = FeedRepository.list end + + def edit + @feed = FeedRepository.fetch(params[:id]) + end + + def update + feed = FeedRepository.fetch(params[:id]) + + FeedRepository.update_feed(feed, params[:feed_name], params[:feed_url], params[:group_id]) + + flash[:success] = t("feeds.edit.flash.updated_successfully") + redirect_to("/feeds") + end end diff --git a/app/controllers/sinatra/feeds_controller.rb b/app/controllers/sinatra/feeds_controller.rb index 8fd63fd36..3425fd090 100644 --- a/app/controllers/sinatra/feeds_controller.rb +++ b/app/controllers/sinatra/feeds_controller.rb @@ -3,21 +3,6 @@ require_relative "../../commands/feeds/export_to_opml" class Stringer < Sinatra::Base - get "/feeds/:id/edit" do - @feed = FeedRepository.fetch(params[:id]) - - erb :'feeds/edit' - end - - put "/feeds/:id" do - feed = FeedRepository.fetch(params[:id]) - - FeedRepository.update_feed(feed, params[:feed_name], params[:feed_url], params[:group_id]) - - flash[:success] = t("feeds.edit.flash.updated_successfully") - redirect to("/feeds") - end - delete "/feeds/:feed_id" do FeedRepository.delete(params[:feed_id]) diff --git a/app/views/feeds/_feed.erb b/app/views/feeds/_feed.html.erb similarity index 100% rename from app/views/feeds/_feed.erb rename to app/views/feeds/_feed.html.erb diff --git a/app/views/feeds/edit.erb b/app/views/feeds/edit.html.erb similarity index 97% rename from app/views/feeds/edit.erb rename to app/views/feeds/edit.html.erb index 7f0565daf..77ae96539 100644 --- a/app/views/feeds/edit.erb +++ b/app/views/feeds/edit.html.erb @@ -1,5 +1,5 @@
    - <%= render_partial :feed_action_bar %> + <%= render "feeds/action_bar" %>
    From 4b8c42374049f7b20e25b6915112ae3a3487b47d Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 12:01:30 -0800 Subject: [PATCH 0397/1107] Rails: use Sinatra hash in controller (#701) We'll be needing a bit more robust flash support in subsequent changes. --- .rubocop.yml | 1 + app/controllers/application_controller.rb | 7 ++++++- spec/controllers/debug_controller_spec.rb | 5 ----- spec/controllers/feeds_controller_spec.rb | 5 ----- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 8c8b74124..80453fba5 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -53,4 +53,5 @@ Style/StringLiterals: Lint/ConstantResolution: { Enabled: false } Rails/SchemaComment: { Enabled: false } +Style/InlineComment: { Enabled: false } Style/RequireOrder: { Enabled: false } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b0599055d..3b6863bfa 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,6 +5,7 @@ class ApplicationController < ActionController::Base helper_method :current_user before_action :append_view_path + after_action :rotate_flash # needed for Sinatra def append_view_path @@ -12,7 +13,11 @@ def append_view_path end def flash - session["flash"] + @flash ||= Sinatra::Flash::FlashHash.new(session[:flash]) end helper_method :flash + + def rotate_flash + session[:flash] = flash.next # for Sinatra + end end diff --git a/spec/controllers/debug_controller_spec.rb b/spec/controllers/debug_controller_spec.rb index 63a3825e3..937ea10d5 100644 --- a/spec/controllers/debug_controller_spec.rb +++ b/spec/controllers/debug_controller_spec.rb @@ -4,11 +4,6 @@ app_require "controllers/debug_controller" describe DebugController do - before do - # for Sinatra - allow_any_instance_of(described_class).to receive(:session).and_return({ "flash" => {} }) - end - describe "GET /debug" do before do delayed_job = double "Delayed::Job" diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index c986e6dd1..22e843dc7 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -5,11 +5,6 @@ describe "FeedsController" do let(:feeds) { [FeedFactory.build, FeedFactory.build] } - before do - # for Sinatra - allow_any_instance_of(FeedsController).to receive(:session).and_return({ "flash" => {} }) - end - describe "GET /feeds" do it "renders a list of feeds" do expect(FeedRepository).to receive(:list).and_return(feeds) From 3ad07bad537928b623a4fa9b8e1edd2c8355088b Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 12:20:24 -0800 Subject: [PATCH 0398/1107] Rails: move feed new/create/destroy to ActionController (#702) --- .rubocop.yml | 1 + .rubocop_todo.yml | 14 +++++----- app.rb | 3 +++ app/controllers/feeds_controller.rb | 28 ++++++++++++++++++++ app/controllers/sinatra/feeds_controller.rb | 29 --------------------- app/views/feeds/{add.erb => new.html.erb} | 2 +- 6 files changed, 39 insertions(+), 38 deletions(-) rename app/views/feeds/{add.erb => new.html.erb} (94%) diff --git a/.rubocop.yml b/.rubocop.yml index 80453fba5..83bd588ff 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -55,3 +55,4 @@ Lint/ConstantResolution: { Enabled: false } Rails/SchemaComment: { Enabled: false } Style/InlineComment: { Enabled: false } Style/RequireOrder: { Enabled: false } +Style/SafeNavigation: { Enabled: false } diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 32562e00f..01b06c0c3 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -79,6 +79,12 @@ Lint/NumberConversion: - 'spec/models/feed_spec.rb' - 'spec/models/story_spec.rb' +# Offense count: 1 +# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, CountRepeatedAttributes, Max. +Metrics/AbcSize: + Exclude: + - 'app/controllers/feeds_controller.rb' + # Offense count: 9 # Configuration parameters: ForbiddenDelimiters. # ForbiddenDelimiters: (?-mix:(^|\s)(EO[A-Z]{1}|END)(\s|$)) @@ -1092,14 +1098,6 @@ Style/ReturnNil: Exclude: - 'app/repositories/user_repository.rb' -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. -# AllowedMethods: present?, blank?, presence, try, try! -Style/SafeNavigation: - Exclude: - - 'app/controllers/sinatra/feeds_controller.rb' - # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Methods. diff --git a/app.rb b/app.rb index d4671f1b7..cd8efd417 100644 --- a/app.rb +++ b/app.rb @@ -102,6 +102,9 @@ def t(*args, **kwargs) rails_route(:get, "/feeds", to: "feeds#index") rails_route(:get, "/feeds/:id/edit", to: "feeds#edit") rails_route(:put, "/feeds/:id", to: "feeds#update") + rails_route(:delete, "/feeds/:id", to: "feeds#destroy") + rails_route(:get, "/feeds/new", to: "feeds#new") + rails_route(:post, "/feeds", to: "feeds#create") end require_relative "app/controllers/sinatra/stories_controller" diff --git a/app/controllers/feeds_controller.rb b/app/controllers/feeds_controller.rb index 7cef7d6d6..2027f0edd 100644 --- a/app/controllers/feeds_controller.rb +++ b/app/controllers/feeds_controller.rb @@ -5,10 +5,32 @@ def index @feeds = FeedRepository.list end + def new + @feed_url = params[:feed_url] + end + def edit @feed = FeedRepository.fetch(params[:id]) end + def create + @feed_url = params[:feed_url] + feed = AddNewFeed.add(@feed_url) + + if feed && feed.valid? + FetchFeeds.enqueue([feed]) + + flash[:success] = t("feeds.add.flash.added_successfully") + redirect_to("/") + elsif feed + flash.now[:error] = t("feeds.add.flash.already_subscribed_error") + render(:new) + else + flash.now[:error] = t("feeds.add.flash.feed_not_found_error") + render(:new) + end + end + def update feed = FeedRepository.fetch(params[:id]) @@ -17,4 +39,10 @@ def update flash[:success] = t("feeds.edit.flash.updated_successfully") redirect_to("/feeds") end + + def destroy + FeedRepository.delete(params[:id]) + + head(:ok) + end end diff --git a/app/controllers/sinatra/feeds_controller.rb b/app/controllers/sinatra/feeds_controller.rb index 3425fd090..1bd6cdade 100644 --- a/app/controllers/sinatra/feeds_controller.rb +++ b/app/controllers/sinatra/feeds_controller.rb @@ -3,35 +3,6 @@ require_relative "../../commands/feeds/export_to_opml" class Stringer < Sinatra::Base - delete "/feeds/:feed_id" do - FeedRepository.delete(params[:feed_id]) - - status 200 - end - - get "/feeds/new" do - @feed_url = params[:feed_url] - erb :'feeds/add' - end - - post "/feeds" do - @feed_url = params[:feed_url] - feed = AddNewFeed.add(@feed_url) - - if feed && feed.valid? - FetchFeeds.enqueue([feed]) - - flash[:success] = t("feeds.add.flash.added_successfully") - redirect to("/") - elsif feed - flash.now[:error] = t("feeds.add.flash.already_subscribed_error") - erb :'feeds/add' - else - flash.now[:error] = t("feeds.add.flash.feed_not_found_error") - erb :'feeds/add' - end - end - get "/feeds/import" do erb :'feeds/import' end diff --git a/app/views/feeds/add.erb b/app/views/feeds/new.html.erb similarity index 94% rename from app/views/feeds/add.erb rename to app/views/feeds/new.html.erb index 7ead0fde7..c9358838a 100644 --- a/app/views/feeds/add.erb +++ b/app/views/feeds/new.html.erb @@ -1,5 +1,5 @@
    - <%= render_partial :feed_action_bar %> + <%= render "feeds/action_bar" %>
    From 85acafafa4597f03cfe7e8cf9a3746f4821ab111 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 15:20:07 -0800 Subject: [PATCH 0399/1107] Rails: move remaining feeds actions to ActionController (#703) --- .rubocop.yml | 5 ++ .rubocop_todo.yml | 74 +++++-------------- app.rb | 6 +- app/controllers/exports_controller.rb | 16 ++++ app/controllers/imports_controller.rb | 11 +++ app/controllers/sinatra/feeds_controller.rb | 22 ------ .../import.erb => imports/new.html.erb} | 0 spec/controllers/exports_controller_spec.rb | 28 +++++++ spec/controllers/feeds_controller_spec.rb | 50 +------------ spec/controllers/imports_controller_spec.rb | 30 ++++++++ 10 files changed, 115 insertions(+), 127 deletions(-) create mode 100644 app/controllers/exports_controller.rb create mode 100644 app/controllers/imports_controller.rb delete mode 100644 app/controllers/sinatra/feeds_controller.rb rename app/views/{feeds/import.erb => imports/new.html.erb} (100%) create mode 100644 spec/controllers/exports_controller_spec.rb create mode 100644 spec/controllers/imports_controller_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 83bd588ff..9b99d5078 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -45,6 +45,11 @@ Style/NumericLiterals: Style/StringLiterals: EnforcedStyle: double_quotes +Style/MethodCallWithArgsParentheses: + AllowedMethods: + - to + - not_to + ################################################################################ # # Rules we don't want to enable diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 01b06c0c3..27b6ffbc0 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 400` -# on 2022-12-18 18:04:01 UTC using RuboCop version 1.40.0. +# on 2022-12-18 23:12:14 UTC using RuboCop version 1.40.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -144,11 +144,10 @@ RSpec/AlignRightLetBrace: - 'spec/tasks/fetch_feeds_spec.rb' - 'spec/utils/feed_discovery_spec.rb' -# Offense count: 7 +# Offense count: 6 RSpec/AnyInstance: Exclude: - - 'spec/controllers/debug_controller_spec.rb' - - 'spec/controllers/feeds_controller_spec.rb' + - 'spec/controllers/exports_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/tasks/fetch_feed_spec.rb' @@ -235,7 +234,7 @@ RSpec/EmptyLineAfterFinalLet: - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/commands/feeds/import_from_opml_spec.rb' - 'spec/commands/users/complete_setup_spec.rb' - - 'spec/controllers/feeds_controller_spec.rb' + - 'spec/controllers/exports_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/models/story_spec.rb' - 'spec/repositories/story_repository_spec.rb' @@ -358,6 +357,7 @@ RSpec/MessageExpectation: - 'spec/commands/users/create_user_spec.rb' - 'spec/controllers/feeds_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' + - 'spec/controllers/imports_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api/authentication_spec.rb' - 'spec/fever_api/read_feeds_spec.rb' @@ -391,6 +391,7 @@ RSpec/MessageSpies: - 'spec/commands/users/create_user_spec.rb' - 'spec/controllers/feeds_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' + - 'spec/controllers/imports_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api/authentication_spec.rb' - 'spec/fever_api/read_feeds_spec.rb' @@ -423,8 +424,10 @@ RSpec/MultipleExpectations: - 'spec/commands/users/complete_setup_spec.rb' - 'spec/commands/users/create_user_spec.rb' - 'spec/controllers/debug_controller_spec.rb' + - 'spec/controllers/exports_controller_spec.rb' - 'spec/controllers/feeds_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' + - 'spec/controllers/imports_controller_spec.rb' - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api/authentication_spec.rb' @@ -515,6 +518,7 @@ RSpec/StubbedMock: - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/commands/stories/mark_group_as_read_spec.rb' - 'spec/commands/users/change_user_password_spec.rb' + - 'spec/controllers/exports_controller_spec.rb' - 'spec/controllers/feeds_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' @@ -597,6 +601,7 @@ Rails/HttpPositionalArguments: - 'spec/controllers/debug_controller_spec.rb' - 'spec/controllers/feeds_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' + - 'spec/controllers/imports_controller_spec.rb' - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api_spec.rb' @@ -734,7 +739,7 @@ Style/FetchEnvVar: Exclude: - 'Rakefile' -# Offense count: 152 +# Offense count: 153 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never @@ -759,7 +764,6 @@ Style/FrozenStringLiteralComment: - 'app/commands/users/create_user.rb' - 'app/commands/users/sign_in_user.rb' - 'app/controllers/debug_controller.rb' - - 'app/controllers/sinatra/feeds_controller.rb' - 'app/controllers/sinatra/first_run_controller.rb' - 'app/controllers/sinatra/sessions_controller.rb' - 'app/controllers/sinatra/stories_controller.rb' @@ -842,8 +846,10 @@ Style/FrozenStringLiteralComment: - 'spec/commands/users/sign_in_user_spec.rb' - 'spec/config/asset_pipeline_spec.rb' - 'spec/controllers/debug_controller_spec.rb' + - 'spec/controllers/exports_controller_spec.rb' - 'spec/controllers/feeds_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' + - 'spec/controllers/imports_controller_spec.rb' - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/factories.rb' @@ -934,12 +940,7 @@ Style/HashSyntax: - 'spec/tasks/change_password_spec.rb' - 'spec/tasks/fetch_feed_spec.rb' -# Offense count: 1 -Style/InlineComment: - Exclude: - - 'app/utils/opml_parser.rb' - -# Offense count: 695 +# Offense count: 187 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: IgnoreMacros, AllowedMethods, IgnoredMethods, AllowedPatterns, IgnoredPatterns, IncludedMacros, AllowParenthesesInMultilineCall, AllowParenthesesInChaining, AllowParenthesesInCamelCaseMethod, AllowParenthesesInStringInterpolation, EnforcedStyle. # SupportedStyles: require_parentheses, omit_parentheses @@ -972,58 +973,29 @@ Style/MethodCallWithArgsParentheses: - 'db/migrate/20130805113712_update_stories_unique_constraints.rb' - 'db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb' - 'db/migrate/20140413100725_add_groups_table_and_foreign_keys_to_feeds.rb' - - 'spec/app_spec.rb' - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/commands/feeds/export_to_opml_spec.rb' - 'spec/commands/feeds/import_from_opml_spec.rb' - 'spec/commands/find_new_stories_spec.rb' - - 'spec/commands/stories/mark_all_as_read_spec.rb' - - 'spec/commands/stories/mark_as_read_spec.rb' - - 'spec/commands/stories/mark_as_starred_spec.rb' - - 'spec/commands/stories/mark_as_unread_spec.rb' - - 'spec/commands/stories/mark_as_unstarred_spec.rb' - - 'spec/commands/stories/mark_feed_as_read_spec.rb' - - 'spec/commands/stories/mark_group_as_read_spec.rb' - 'spec/commands/users/change_user_password_spec.rb' - - 'spec/commands/users/complete_setup_spec.rb' - - 'spec/commands/users/create_user_spec.rb' - 'spec/commands/users/sign_in_user_spec.rb' - - 'spec/config/asset_pipeline_spec.rb' - 'spec/controllers/debug_controller_spec.rb' + - 'spec/controllers/exports_controller_spec.rb' - 'spec/controllers/feeds_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' + - 'spec/controllers/imports_controller_spec.rb' - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - - 'spec/fever_api/authentication_spec.rb' - - 'spec/fever_api/read_favicons_spec.rb' - - 'spec/fever_api/read_feeds_groups_spec.rb' - - 'spec/fever_api/read_feeds_spec.rb' - - 'spec/fever_api/read_groups_spec.rb' - - 'spec/fever_api/read_items_spec.rb' - - 'spec/fever_api/read_links_spec.rb' - - 'spec/fever_api/sync_saved_item_ids_spec.rb' - - 'spec/fever_api/sync_unread_item_ids_spec.rb' - - 'spec/fever_api/write_mark_feed_spec.rb' - - 'spec/fever_api/write_mark_group_spec.rb' - - 'spec/fever_api/write_mark_item_spec.rb' - 'spec/fever_api_spec.rb' - 'spec/helpers/authentications_helper_spec.rb' - 'spec/helpers/url_helpers_spec.rb' - 'spec/integration/feed_importing_spec.rb' - 'spec/javascript/test_controller.rb' - - 'spec/models/feed_spec.rb' - 'spec/models/group_spec.rb' - - 'spec/models/migration_status_spec.rb' - 'spec/models/story_spec.rb' - 'spec/repositories/feed_repository_spec.rb' - - 'spec/repositories/group_repository_spec.rb' - 'spec/repositories/story_repository_spec.rb' - - 'spec/repositories/user_repository_spec.rb' - 'spec/spec_helper.rb' - - 'spec/tasks/change_password_spec.rb' - - 'spec/tasks/fetch_feed_spec.rb' - - 'spec/tasks/fetch_feeds_spec.rb' - - 'spec/tasks/remove_old_stories_spec.rb' - 'spec/utils/content_sanitizer_spec.rb' - 'spec/utils/feed_discovery_spec.rb' - 'spec/utils/i18n_support_spec.rb' @@ -1037,7 +1009,7 @@ Style/NumericPredicate: Exclude: - 'app/commands/stories/mark_group_as_read.rb' -# Offense count: 4 +# Offense count: 6 Style/OpenStructUse: Exclude: - 'app.rb' @@ -1082,14 +1054,6 @@ Style/PercentLiteralDelimiters: - 'spec/helpers/url_helpers_spec.rb' - 'spec/javascript/test_controller.rb' -# Offense count: 6 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: same_as_string_literals, single_quotes, double_quotes -Style/QuotedSymbols: - Exclude: - - 'app/controllers/sinatra/feeds_controller.rb' - # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. @@ -1121,7 +1085,7 @@ Style/StaticClass: - 'app/utils/api_key.rb' - 'app/utils/content_sanitizer.rb' -# Offense count: 20 +# Offense count: 19 # This cop supports unsafe autocorrection (--autocorrect-all). Style/StringHashKeys: Exclude: @@ -1129,8 +1093,8 @@ Style/StringHashKeys: - 'fever_api.rb' - 'spec/app_spec.rb' - 'spec/controllers/debug_controller_spec.rb' - - 'spec/controllers/feeds_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' + - 'spec/controllers/imports_controller_spec.rb' - 'spec/controllers/sessions_controller_spec.rb' - 'spec/fever_api/read_favicons_spec.rb' - 'spec/fever_api/read_feeds_groups_spec.rb' diff --git a/app.rb b/app.rb index cd8efd417..8b4557e5c 100644 --- a/app.rb +++ b/app.rb @@ -22,6 +22,8 @@ require_relative "app/controllers/application_controller" require_relative "app/controllers/debug_controller" require_relative "app/controllers/feeds_controller" +require_relative "app/controllers/exports_controller" +require_relative "app/controllers/imports_controller" module Rails def self.application @@ -105,9 +107,11 @@ def t(*args, **kwargs) rails_route(:delete, "/feeds/:id", to: "feeds#destroy") rails_route(:get, "/feeds/new", to: "feeds#new") rails_route(:post, "/feeds", to: "feeds#create") + rails_route(:get, "/feeds/export", to: "exports#index") + rails_route(:get, "/feeds/import", to: "imports#new") + rails_route(:post, "/feeds/import", to: "imports#create") end require_relative "app/controllers/sinatra/stories_controller" require_relative "app/controllers/sinatra/first_run_controller" require_relative "app/controllers/sinatra/sessions_controller" -require_relative "app/controllers/sinatra/feeds_controller" diff --git a/app/controllers/exports_controller.rb b/app/controllers/exports_controller.rb new file mode 100644 index 000000000..f7a58f99f --- /dev/null +++ b/app/controllers/exports_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "../commands/feeds/export_to_opml" + +class ExportsController < ApplicationController + def index + xml = ExportToOpml.new(Feed.all).to_xml + + send_data( + xml, + type: "application/xml", + disposition: "attachment", + filename: "stringer.opml" + ) + end +end diff --git a/app/controllers/imports_controller.rb b/app/controllers/imports_controller.rb new file mode 100644 index 000000000..83c3ef2a3 --- /dev/null +++ b/app/controllers/imports_controller.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class ImportsController < ApplicationController + def new; end + + def create + ImportFromOpml.import(params["opml_file"].read) + + redirect_to("/setup/tutorial") + end +end diff --git a/app/controllers/sinatra/feeds_controller.rb b/app/controllers/sinatra/feeds_controller.rb deleted file mode 100644 index 1bd6cdade..000000000 --- a/app/controllers/sinatra/feeds_controller.rb +++ /dev/null @@ -1,22 +0,0 @@ -require_relative "../../repositories/feed_repository" -require_relative "../../commands/feeds/add_new_feed" -require_relative "../../commands/feeds/export_to_opml" - -class Stringer < Sinatra::Base - get "/feeds/import" do - erb :'feeds/import' - end - - post "/feeds/import" do - ImportFromOpml.import(params["opml_file"][:tempfile].read) - - redirect to("/setup/tutorial") - end - - get "/feeds/export" do - content_type "application/xml" - attachment "stringer.opml" - - ExportToOpml.new(Feed.all).to_xml - end -end diff --git a/app/views/feeds/import.erb b/app/views/imports/new.html.erb similarity index 100% rename from app/views/feeds/import.erb rename to app/views/imports/new.html.erb diff --git a/spec/controllers/exports_controller_spec.rb b/spec/controllers/exports_controller_spec.rb new file mode 100644 index 000000000..f4f0016be --- /dev/null +++ b/spec/controllers/exports_controller_spec.rb @@ -0,0 +1,28 @@ +require "spec_helper" + +app_require "controllers/exports_controller" + +describe ExportsController do + describe "GET /feeds/export" do + let(:some_xml) { "some dummy opml" } + before { allow(Feed).to receive(:all) } + + it "returns an OPML file" do + expect_any_instance_of(ExportToOpml).to receive(:to_xml).and_return(some_xml) + + get "/feeds/export" + + expect(last_response.body).to eq some_xml + end + + it "responds with OPML headers" do + expect_any_instance_of(ExportToOpml).to receive(:to_xml).and_return(some_xml) + + get "/feeds/export" + + expect(last_response.header["Content-Type"]).to include "application/xml" + expect(last_response.header["Content-Disposition"]) + .to eq("attachment; filename=\"stringer.opml\"; filename*=UTF-8''stringer.opml") + end + end +end diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index 22e843dc7..80290eb8d 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -app_require "controllers/sinatra/feeds_controller" +app_require "controllers/feeds_controller" describe "FeedsController" do let(:feeds) { [FeedFactory.build, FeedFactory.build] } @@ -134,52 +134,4 @@ def params(feed, **overrides) end end end - - describe "GET /feeds/import" do - it "displays the import options" do - get "/feeds/import" - - page = last_response.body - expect(page).to have_tag("input#opml_file") - expect(page).to have_tag("a#skip") - end - end - - describe "POST /feeds/import" do - let(:opml_file) do - Rack::Test::UploadedFile.new("spec/sample_data/subscriptions.xml", "application/xml") - end - - it "parse OPML and starts fetching" do - expect(ImportFromOpml).to receive(:import).once - - post "/feeds/import", "opml_file" => opml_file - - expect(last_response.status).to be 302 - expect(URI.parse(last_response.location).path).to eq "/setup/tutorial" - end - end - - describe "GET /feeds/export" do - let(:some_xml) { "some dummy opml" } - before { allow(Feed).to receive(:all) } - - it "returns an OPML file" do - expect_any_instance_of(ExportToOpml).to receive(:to_xml).and_return(some_xml) - - get "/feeds/export" - - expect(last_response.body).to eq some_xml - end - - it "responds with OPML headers" do - expect_any_instance_of(ExportToOpml).to receive(:to_xml).and_return(some_xml) - - get "/feeds/export" - - expect(last_response.header["Content-Type"]).to include "application/xml" - expect(last_response.header["Content-Disposition"]) - .to eq("attachment; filename=\"stringer.opml\"") - end - end end diff --git a/spec/controllers/imports_controller_spec.rb b/spec/controllers/imports_controller_spec.rb new file mode 100644 index 000000000..f6df9f35d --- /dev/null +++ b/spec/controllers/imports_controller_spec.rb @@ -0,0 +1,30 @@ +require "spec_helper" + +app_require "controllers/imports_controller" + +describe ImportsController do + describe "GET /feeds/import" do + it "displays the import options" do + get "/feeds/import" + + page = last_response.body + expect(page).to have_tag("input#opml_file") + expect(page).to have_tag("a#skip") + end + end + + describe "POST /feeds/import" do + let(:opml_file) do + Rack::Test::UploadedFile.new("spec/sample_data/subscriptions.xml", "application/xml") + end + + it "parse OPML and starts fetching" do + expect(ImportFromOpml).to receive(:import).once + + post "/feeds/import", "opml_file" => opml_file + + expect(last_response.status).to be 302 + expect(URI.parse(last_response.location).path).to eq "/setup/tutorial" + end + end +end From a5d86ed80a7e2d5a3280f6ff743862442594195f Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 15:28:27 -0800 Subject: [PATCH 0400/1107] Specs: eliminate logger output from tests (#704) --- spec/tasks/fetch_feed_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/tasks/fetch_feed_spec.rb b/spec/tasks/fetch_feed_spec.rb index 1c1ff0f8a..f8095025f 100644 --- a/spec/tasks/fetch_feed_spec.rb +++ b/spec/tasks/fetch_feed_spec.rb @@ -25,7 +25,7 @@ expect(StoryRepository).not_to receive(:add) - FetchFeed.new(daring_fireball, parser: parser, client: client).fetch + FetchFeed.new(daring_fireball, parser: parser, client: client, logger: nil).fetch end end @@ -98,7 +98,7 @@ expect(FeedRepository).to receive(:set_status) .with(:red, daring_fireball) - FetchFeed.new(daring_fireball, parser: parser, client: client).fetch + FetchFeed.new(daring_fireball, parser: parser, client: client, logger: nil).fetch end end end From 5da8a6fc02ff93a502f2c123a5aa61318d25674b Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 18:44:42 -0800 Subject: [PATCH 0401/1107] Coverage: enable branch coverage for tests (#705) --- .rubocop.yml | 1 + .rubocop_todo.yml | 21 ++++++++++++--------- spec/jobs/fetch_feed_job_spec.rb | 15 +++++++++++++++ spec/support/coverage.rb | 9 +++------ 4 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 spec/jobs/fetch_feed_job_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 9b99d5078..860a6184d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -49,6 +49,7 @@ Style/MethodCallWithArgsParentheses: AllowedMethods: - to - not_to + - describe ################################################################################ # diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 27b6ffbc0..daafc95d8 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 400` -# on 2022-12-18 23:12:14 UTC using RuboCop version 1.40.0. +# on 2022-12-19 02:34:30 UTC using RuboCop version 1.40.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -343,7 +343,7 @@ RSpec/MessageChain: - 'spec/fever_api/read_feeds_groups_spec.rb' - 'spec/fever_api_spec.rb' -# Offense count: 100 +# Offense count: 102 # Configuration parameters: EnforcedStyle. # SupportedStyles: allow, expect RSpec/MessageExpectation: @@ -369,6 +369,7 @@ RSpec/MessageExpectation: - 'spec/fever_api/write_mark_group_spec.rb' - 'spec/fever_api/write_mark_item_spec.rb' - 'spec/fever_api_spec.rb' + - 'spec/jobs/fetch_feed_job_spec.rb' - 'spec/repositories/feed_repository_spec.rb' - 'spec/repositories/story_repository_spec.rb' - 'spec/tasks/change_password_spec.rb' @@ -377,7 +378,7 @@ RSpec/MessageExpectation: - 'spec/tasks/remove_old_stories_spec.rb' - 'spec/utils/feed_discovery_spec.rb' -# Offense count: 105 +# Offense count: 107 # Configuration parameters: EnforcedStyle. # SupportedStyles: have_received, receive RSpec/MessageSpies: @@ -403,6 +404,7 @@ RSpec/MessageSpies: - 'spec/fever_api/write_mark_group_spec.rb' - 'spec/fever_api/write_mark_item_spec.rb' - 'spec/fever_api_spec.rb' + - 'spec/jobs/fetch_feed_job_spec.rb' - 'spec/repositories/feed_repository_spec.rb' - 'spec/repositories/story_repository_spec.rb' - 'spec/tasks/change_password_spec.rb' @@ -411,7 +413,7 @@ RSpec/MessageSpies: - 'spec/tasks/remove_old_stories_spec.rb' - 'spec/utils/feed_discovery_spec.rb' -# Offense count: 96 +# Offense count: 97 # Configuration parameters: Max. RSpec/MultipleExpectations: Exclude: @@ -440,6 +442,7 @@ RSpec/MultipleExpectations: - 'spec/fever_api/write_mark_group_spec.rb' - 'spec/fever_api/write_mark_item_spec.rb' - 'spec/fever_api_spec.rb' + - 'spec/jobs/fetch_feed_job_spec.rb' - 'spec/repositories/feed_repository_spec.rb' - 'spec/repositories/story_repository_spec.rb' - 'spec/tasks/fetch_feed_spec.rb' @@ -512,7 +515,7 @@ RSpec/ScatteredLet: - 'spec/commands/feeds/import_from_opml_spec.rb' - 'spec/repositories/feed_repository_spec.rb' -# Offense count: 53 +# Offense count: 55 RSpec/StubbedMock: Exclude: - 'spec/commands/feeds/add_new_feed_spec.rb' @@ -532,11 +535,12 @@ RSpec/StubbedMock: - 'spec/fever_api/write_mark_group_spec.rb' - 'spec/fever_api/write_mark_item_spec.rb' - 'spec/fever_api_spec.rb' + - 'spec/jobs/fetch_feed_job_spec.rb' - 'spec/repositories/feed_repository_spec.rb' - 'spec/tasks/remove_old_stories_spec.rb' - 'spec/utils/feed_discovery_spec.rb' -# Offense count: 92 +# Offense count: 93 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Exclude: @@ -560,6 +564,7 @@ RSpec/VerifiedDoubles: - 'spec/fever_api/write_mark_group_spec.rb' - 'spec/fever_api/write_mark_item_spec.rb' - 'spec/fever_api_spec.rb' + - 'spec/jobs/fetch_feed_job_spec.rb' - 'spec/repositories/story_repository_spec.rb' - 'spec/tasks/fetch_feed_spec.rb' - 'spec/tasks/remove_old_stories_spec.rb' @@ -940,7 +945,7 @@ Style/HashSyntax: - 'spec/tasks/change_password_spec.rb' - 'spec/tasks/fetch_feed_spec.rb' -# Offense count: 187 +# Offense count: 184 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: IgnoreMacros, AllowedMethods, IgnoredMethods, AllowedPatterns, IgnoredPatterns, IncludedMacros, AllowParenthesesInMultilineCall, AllowParenthesesInChaining, AllowParenthesesInCamelCaseMethod, AllowParenthesesInStringInterpolation, EnforcedStyle. # SupportedStyles: require_parentheses, omit_parentheses @@ -987,11 +992,9 @@ Style/MethodCallWithArgsParentheses: - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api_spec.rb' - - 'spec/helpers/authentications_helper_spec.rb' - 'spec/helpers/url_helpers_spec.rb' - 'spec/integration/feed_importing_spec.rb' - 'spec/javascript/test_controller.rb' - - 'spec/models/group_spec.rb' - 'spec/models/story_spec.rb' - 'spec/repositories/feed_repository_spec.rb' - 'spec/repositories/story_repository_spec.rb' diff --git a/spec/jobs/fetch_feed_job_spec.rb b/spec/jobs/fetch_feed_job_spec.rb new file mode 100644 index 000000000..f3db61959 --- /dev/null +++ b/spec/jobs/fetch_feed_job_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "spec_helper" + +app_require "jobs/fetch_feed_job" + +RSpec.describe FetchFeedJob do + it "fetches feeds" do + job = described_class.new(123) + feed = Feed.new(id: 123, url: "http://example.com/feed") + expect(FeedRepository).to receive(:fetch).with(123).and_return(feed) + expect(FetchFeed).to receive(:new).with(feed).and_return(double(fetch: nil)) + job.perform + end +end diff --git a/spec/support/coverage.rb b/spec/support/coverage.rb index 03b601a45..441491114 100644 --- a/spec/support/coverage.rb +++ b/spec/support/coverage.rb @@ -5,15 +5,12 @@ SimpleCov.formatter = Coveralls::SimpleCov::Formatter end -SimpleCov.start("test_frameworks") do +SimpleCov.start(:rails) do add_group("Commands", "app/commands") - add_group("Controllers", "app/controllers") add_group("Fever API", "app/fever_api") - add_group("Helpers", "app/helpers") - add_group("Models", "app/models") add_group("Repositories", "app/repositories") add_group("Tasks", "app/tasks") add_group("Utils", "app/utils") - add_filter("/db/migrate/") + enable_coverage :branch end -SimpleCov.minimum_coverage(100) +SimpleCov.minimum_coverage(line: 100, branch: 91) From 0a81d75f7d6ce4bba0304c8afc9bf34c8bc9ef5b Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 18:55:51 -0800 Subject: [PATCH 0402/1107] Coverage: add branch tests for FetchFeeds (#706) --- .rubocop.yml | 1 + spec/tasks/fetch_feeds_spec.rb | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 860a6184d..53befaa58 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -59,6 +59,7 @@ Style/MethodCallWithArgsParentheses: Lint/ConstantResolution: { Enabled: false } Rails/SchemaComment: { Enabled: false } +RSpec/StubbedMock: { Enabled: false } Style/InlineComment: { Enabled: false } Style/RequireOrder: { Enabled: false } Style/SafeNavigation: { Enabled: false } diff --git a/spec/tasks/fetch_feeds_spec.rb b/spec/tasks/fetch_feeds_spec.rb index 9a7ce4ffd..08a3325f7 100644 --- a/spec/tasks/fetch_feeds_spec.rb +++ b/spec/tasks/fetch_feeds_spec.rb @@ -4,8 +4,8 @@ describe FetchFeeds do describe "#fetch_all" do let(:feeds) { [FeedFactory.build, FeedFactory.build] } - let(:fetcher_one) { double } - let(:fetcher_two) { double } + let(:fetcher_one) { instance_double(FetchFeed) } + let(:fetcher_two) { instance_double(FetchFeed) } let(:pool) { double } it "calls FetchFeed#fetch for every feed" do @@ -19,6 +19,18 @@ FetchFeeds.new(feeds, pool).fetch_all end + + it "finds feeds when run after a delay" do + allow(pool).to receive(:process).and_yield + allow(FetchFeed).to receive(:new).and_return(fetcher_one, fetcher_two) + expect(FeedRepository).to receive(:fetch_by_ids).with(feeds.map(&:id)).and_return(feeds) + expect(fetcher_one).to receive(:fetch).once + expect(fetcher_two).to receive(:fetch).once + + expect(pool).to receive(:shutdown) + + FetchFeeds.new(feeds, pool).prepare_to_delay.fetch_all + end end describe "#prepare_to_delay" do From 6634332a697ba3b7e90a2fe3d0aaa44e0154807d Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 19:01:12 -0800 Subject: [PATCH 0403/1107] Coverage: add branch coverage for Story (#707) --- spec/models/story_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/models/story_spec.rb b/spec/models/story_spec.rb index 2375d0b29..413e97558 100644 --- a/spec/models/story_spec.rb +++ b/spec/models/story_spec.rb @@ -131,5 +131,15 @@ created_on_time: published_at.to_i ) end + + it "returns is_read as 0 if story is unread" do + story = create_story(is_read: false) + expect(story.as_fever_json[:is_read]).to eq(0) + end + + it "returns is_saved as 1 if story is starred" do + story = create_story(is_starred: true) + expect(story.as_fever_json[:is_saved]).to eq(1) + end end end From c7eb0dfea57d0236769de00c213d7b409cd52a75 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 19:07:09 -0800 Subject: [PATCH 0404/1107] Coverage: add branch coverage for FetchFeed (#708) --- spec/tasks/fetch_feed_spec.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/spec/tasks/fetch_feed_spec.rb b/spec/tasks/fetch_feed_spec.rb index f8095025f..36c7be165 100644 --- a/spec/tasks/fetch_feed_spec.rb +++ b/spec/tasks/fetch_feed_spec.rb @@ -27,6 +27,17 @@ FetchFeed.new(daring_fireball, parser: parser, client: client, logger: nil).fetch end + + it "logs a message" do + client = class_spy(HTTParty) + parser = class_double(Feedjira, parse: 304) + output = StringIO.new + logger = Logger.new(output) + + FetchFeed.new(daring_fireball, parser: parser, client: client, logger:).fetch + + expect(output.string).to include("has not been modified") + end end context "when no new posts have been added" do @@ -100,6 +111,17 @@ FetchFeed.new(daring_fireball, parser: parser, client: client, logger: nil).fetch end + + it "outputs a message when things go wrong" do + client = class_spy(HTTParty) + parser = class_double(Feedjira, parse: 404) + output = StringIO.new + logger = Logger.new(output) + + FetchFeed.new(daring_fireball, parser: parser, client: client, logger: logger).fetch + + expect(output.string).to include("Something went wrong") + end end end end From 337f9397707f190769296eebb8060317603c69bd Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 20:08:37 -0800 Subject: [PATCH 0405/1107] Coverage: remove unused branch from FeedDiscovery (#709) --- app/utils/feed_discovery.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils/feed_discovery.rb b/app/utils/feed_discovery.rb index dd65d4740..c55ad3484 100644 --- a/app/utils/feed_discovery.rb +++ b/app/utils/feed_discovery.rb @@ -20,6 +20,6 @@ def get_feed_for_url(url, parser, client) feed.feed_url ||= url feed rescue StandardError - yield if block_given? + yield end end From d923992fa4ef4321296bdc15c53351a6d5698574 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 20:13:37 -0800 Subject: [PATCH 0406/1107] Coverage: remove unused condition from MarkGroupAsRead (#710) It shouldn't be possible to get this far unless `group_id` is greater than 0. --- app/commands/stories/mark_group_as_read.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/commands/stories/mark_group_as_read.rb b/app/commands/stories/mark_group_as_read.rb index ff24c9ce8..c177e49b6 100644 --- a/app/commands/stories/mark_group_as_read.rb +++ b/app/commands/stories/mark_group_as_read.rb @@ -15,7 +15,7 @@ def mark_group_as_read if [KINDLING_GROUP_ID, SPARKS_GROUP_ID].include?(@group_id.to_i) @repo.fetch_unread_by_timestamp(@timestamp).update_all(is_read: true) - elsif @group_id.to_i > 0 + else @repo.fetch_unread_by_timestamp_and_group( @timestamp, @group_id From f009b39efb8d0bc4acbe37268938ded851325080 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 20:30:47 -0800 Subject: [PATCH 0407/1107] Coverage: update branch coverage for WriteMarkItem (#711) --- spec/fever_api/write_mark_item_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/fever_api/write_mark_item_spec.rb b/spec/fever_api/write_mark_item_spec.rb index 2a2929409..ef70c519f 100644 --- a/spec/fever_api/write_mark_item_spec.rb +++ b/spec/fever_api/write_mark_item_spec.rb @@ -55,6 +55,6 @@ end it "returns an empty hash otherwise" do - expect(subject.call).to eq({}) + expect(subject.call({ mark: "item" })).to eq({}) end end From 1b7713ecaca06afb65cafa908a718afc0c7e6a73 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 21:41:37 -0800 Subject: [PATCH 0408/1107] Coverage: cover branch in StoryRepository (#712) --- spec/repositories/story_repository_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 47e6af66c..92e457bd1 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -459,6 +459,12 @@ expect(StoryRepository.extract_content(summary_only)).to eq "Dumb publisher" end + it "returns empty string if there is no content or summary" do + entry = double(url: "http://mdswanson.com", content: nil, summary: nil) + + expect(StoryRepository.extract_content(entry)).to eq "" + end + it "expands urls" do entry = double( url: "http://mdswanson.com", From 74a603268134325e18bdc4c261ca569d7500b73e Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 21:46:49 -0800 Subject: [PATCH 0409/1107] Coverage: remove unused clause in importer (#713) Based on the implementation of `OpmlParser` it doesn't look like there's any way to hit this guard clause. --- app/commands/feeds/import_from_opml.rb | 2 -- spec/commands/feeds/import_from_opml_spec.rb | 26 +++++++++++++++++--- spec/support/coverage.rb | 2 +- spec/support/files/subscriptions.xml | 2 ++ 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/app/commands/feeds/import_from_opml.rb b/app/commands/feeds/import_from_opml.rb index 9fb6ae47b..8a3841ba9 100644 --- a/app/commands/feeds/import_from_opml.rb +++ b/app/commands/feeds/import_from_opml.rb @@ -14,8 +14,6 @@ def import(opml_contents) # for existing feeds. Feeds without groups are in 'Ungrouped' group, we don't # create such group and create such feeds with group_id = nil. feeds_with_groups.each do |group_name, parsed_feeds| - next if parsed_feeds.empty? - group = Group.where(name: group_name).first_or_create unless group_name == "Ungrouped" parsed_feeds.each { |parsed_feed| create_feed(parsed_feed, group) } diff --git a/spec/commands/feeds/import_from_opml_spec.rb b/spec/commands/feeds/import_from_opml_spec.rb index 6516ece9e..be2e7a9e4 100644 --- a/spec/commands/feeds/import_from_opml_spec.rb +++ b/spec/commands/feeds/import_from_opml_spec.rb @@ -29,19 +29,24 @@ def import url: "http://feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots" ) end - before { import } it "retains exising feeds" do + described_class.import(subscriptions) + expect(feed1).to be_valid expect(feed2).to be_valid end it "creates new groups" do + described_class.import(subscriptions) + expect(group1).to be expect(group2).to be end it "sets group_id for existing feeds" do + described_class.import(subscriptions) + expect(feed1.reload.group).to eq group1 expect(feed2.reload.group).to eq group2 end @@ -57,35 +62,48 @@ def import url: "http://feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots" ) end - before { import } it "creates groups" do + described_class.import(subscriptions) + expect(group1).to be expect(group1).to be end it "creates feeds" do + described_class.import(subscriptions) + expect(feed1).to exist expect(feed2).to exist end it "sets group" do + described_class.import(subscriptions) + expect(feed1.first.group).to eq group1 expect(feed2.first.group).to eq group2 end + + it "does not create empty group" do + described_class.import(subscriptions) + + expect(Group.find_by_name("Empty Group")).to be_nil + end end context "creates new feeds without group" do let(:feed1) { Feed.where(name: "Autoblog", url: "http://feeds.autoblog.com/weblogsinc/autoblog/").first } let(:feed2) { Feed.where(name: "City Guide News", url: "http://www.probki.net/news/RSS_news_feed.asp").first } - before { import } - it "does not create any new group for feeds without group" do + described_class.import(subscriptions) + expect(Group.where("id NOT IN (?)", [group1.id, group2.id]).count).to eq 0 end it "creates feeds without group_id" do + described_class.import(subscriptions) + expect(feed1.group_id).to be_nil expect(feed2.group_id).to be_nil end diff --git a/spec/support/coverage.rb b/spec/support/coverage.rb index 441491114..2f2a2b83b 100644 --- a/spec/support/coverage.rb +++ b/spec/support/coverage.rb @@ -13,4 +13,4 @@ add_group("Utils", "app/utils") enable_coverage :branch end -SimpleCov.minimum_coverage(line: 100, branch: 91) +SimpleCov.minimum_coverage(line: 100, branch: 99) diff --git a/spec/support/files/subscriptions.xml b/spec/support/files/subscriptions.xml index bb44ada99..832f59b48 100755 --- a/spec/support/files/subscriptions.xml +++ b/spec/support/files/subscriptions.xml @@ -23,5 +23,7 @@ + + From 7a459f5c1483595f3d7d90abf833e13e78c2f58f Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 21:55:26 -0800 Subject: [PATCH 0410/1107] RuboCop: reduce line length to 90 (#689) --- .rubocop.yml | 2 +- app/commands/feeds/import_from_opml.rb | 6 ++++-- app/commands/stories/mark_feed_as_read.rb | 4 +++- app/controllers/feeds_controller.rb | 7 ++++++- app/fever_api/read_favicons.rb | 4 +++- app/helpers/controller_helpers.rb | 3 ++- app/repositories/story_repository.rb | 11 +++++------ app/tasks/fetch_feeds.rb | 4 +++- spec/controllers/imports_controller_spec.rb | 5 ++++- spec/controllers/stories_controller_spec.rb | 5 ++++- spec/fever_api/read_favicons_spec.rb | 2 +- spec/fever_api/read_items_spec.rb | 3 ++- spec/fever_api/sync_saved_item_ids_spec.rb | 3 ++- spec/fever_api/sync_unread_item_ids_spec.rb | 3 ++- spec/fever_api_spec.rb | 9 ++++++--- spec/integration/feed_importing_spec.rb | 14 +++++++------- spec/javascript/test_controller.rb | 9 ++++++++- spec/repositories/story_repository_spec.rb | 6 +++++- spec/tasks/fetch_feed_spec.rb | 10 ++++++++-- spec/tasks/fetch_feeds_spec.rb | 3 ++- spec/tasks/remove_old_stories_spec.rb | 3 ++- 21 files changed, 80 insertions(+), 36 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 53befaa58..64a7ed716 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,7 +12,7 @@ AllCops: - 'vendor/**/*' Layout/LineLength: - Max: 95 + Max: 90 Metrics/BlockLength: Exclude: diff --git a/app/commands/feeds/import_from_opml.rb b/app/commands/feeds/import_from_opml.rb index 8a3841ba9..b0133e20e 100644 --- a/app/commands/feeds/import_from_opml.rb +++ b/app/commands/feeds/import_from_opml.rb @@ -14,7 +14,9 @@ def import(opml_contents) # for existing feeds. Feeds without groups are in 'Ungrouped' group, we don't # create such group and create such feeds with group_id = nil. feeds_with_groups.each do |group_name, parsed_feeds| - group = Group.where(name: group_name).first_or_create unless group_name == "Ungrouped" + unless group_name == "Ungrouped" + group = Group.where(name: group_name).first_or_create + end parsed_feeds.each { |parsed_feed| create_feed(parsed_feed, group) } end @@ -23,7 +25,7 @@ def import(opml_contents) private def create_feed(parsed_feed, group) - feed = Feed.where(name: parsed_feed[:name], url: parsed_feed[:url]).first_or_initialize + feed = Feed.where(parsed_feed.slice(:name, :url)).first_or_initialize feed.last_fetched = Time.now - ONE_DAY if feed.new_record? feed.group_id = group.id if group feed.save diff --git a/app/commands/stories/mark_feed_as_read.rb b/app/commands/stories/mark_feed_as_read.rb index 035f50e1d..8c28b8dbe 100644 --- a/app/commands/stories/mark_feed_as_read.rb +++ b/app/commands/stories/mark_feed_as_read.rb @@ -8,6 +8,8 @@ def initialize(feed_id, timestamp, repository = StoryRepository) end def mark_feed_as_read - @repo.fetch_unread_for_feed_by_timestamp(@feed_id, @timestamp).update_all(is_read: true) + @repo + .fetch_unread_for_feed_by_timestamp(@feed_id, @timestamp) + .update_all(is_read: true) end end diff --git a/app/controllers/feeds_controller.rb b/app/controllers/feeds_controller.rb index 2027f0edd..0f1488707 100644 --- a/app/controllers/feeds_controller.rb +++ b/app/controllers/feeds_controller.rb @@ -34,7 +34,12 @@ def create def update feed = FeedRepository.fetch(params[:id]) - FeedRepository.update_feed(feed, params[:feed_name], params[:feed_url], params[:group_id]) + FeedRepository.update_feed( + feed, + params[:feed_name], + params[:feed_url], + params[:group_id] + ) flash[:success] = t("feeds.edit.flash.updated_successfully") redirect_to("/feeds") diff --git a/app/fever_api/read_favicons.rb b/app/fever_api/read_favicons.rb index f49393198..c924b7cfd 100644 --- a/app/fever_api/read_favicons.rb +++ b/app/fever_api/read_favicons.rb @@ -1,5 +1,7 @@ module FeverAPI class ReadFavicons + ICON = "R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==".freeze + def call(params = {}) if params.keys.include?("favicons") { favicons: favicons } @@ -14,7 +16,7 @@ def favicons [ { id: 0, - data: "image/gif;base64,R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" + data: "image/gif;base64,#{ICON}" } ] end diff --git a/app/helpers/controller_helpers.rb b/app/helpers/controller_helpers.rb index efee39f79..4070c9c0e 100644 --- a/app/helpers/controller_helpers.rb +++ b/app/helpers/controller_helpers.rb @@ -8,7 +8,8 @@ def rails_route(method, path, options) controller_name, action_name = to.split("#") controller_klass = "#{controller_name.camelize}Controller".constantize route(method.to_s.upcase, path, options) do - # Make sure that our parsed URL params are where Rack (and ActionDispatch) expect them + # Make sure that our parsed URL params are where Rack (and + # ActionDispatch) expect them app = controller_klass.action(action_name) app.call(request.env.merge("rack.request.query_hash" => params)) end diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index a85019a8a..941d2cd3f 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -40,7 +40,9 @@ def self.fetch_unread_by_timestamp_and_group(timestamp, group_id) def self.fetch_unread_for_feed_by_timestamp(feed_id, timestamp) timestamp = Time.at(timestamp.to_i) - Story.where(feed_id: feed_id).where("created_at < ? AND is_read = ?", timestamp, false) + Story + .where(feed_id: feed_id) + .where("created_at < ? AND is_read = ?", timestamp, false) end def self.save(story) @@ -95,11 +97,8 @@ def self.extract_url(entry, feed) def self.extract_content(entry) sanitized_content = "" - if entry.content - sanitized_content = ContentSanitizer.sanitize(entry.content) - elsif entry.summary - sanitized_content = ContentSanitizer.sanitize(entry.summary) - end + content = entry.content || entry.summary + sanitized_content = ContentSanitizer.sanitize(content) if content if entry.url.present? expand_absolute_urls(sanitized_content, entry.url) diff --git a/app/tasks/fetch_feeds.rb b/app/tasks/fetch_feeds.rb index c1c1aab66..bb7867525 100644 --- a/app/tasks/fetch_feeds.rb +++ b/app/tasks/fetch_feeds.rb @@ -14,7 +14,9 @@ def initialize(feeds, pool = nil) def fetch_all @pool ||= Thread.pool(10) - @feeds = FeedRepository.fetch_by_ids(@feeds_ids) if @feeds.blank? && !@feeds_ids.blank? + if @feeds.blank? && !@feeds_ids.blank? + @feeds = FeedRepository.fetch_by_ids(@feeds_ids) + end @feeds.each do |feed| @pool.process do diff --git a/spec/controllers/imports_controller_spec.rb b/spec/controllers/imports_controller_spec.rb index f6df9f35d..b62500cd4 100644 --- a/spec/controllers/imports_controller_spec.rb +++ b/spec/controllers/imports_controller_spec.rb @@ -15,7 +15,10 @@ describe "POST /feeds/import" do let(:opml_file) do - Rack::Test::UploadedFile.new("spec/sample_data/subscriptions.xml", "application/xml") + Rack::Test::UploadedFile.new( + "spec/sample_data/subscriptions.xml", + "application/xml" + ) end it "parse OPML and starts fetching" do diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index f1a4ea52e..47a668d5f 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -44,7 +44,10 @@ page = last_response.body expect(page).to have_tag("a", with: { href: "/feeds/export" }) expect(page).to have_tag("a", with: { href: "/logout" }) - expect(page).to have_tag("a", with: { href: "https://github.com/stringer-rss/stringer" }) + expect(page).to have_tag( + "a", + with: { href: "https://github.com/stringer-rss/stringer" } + ) end it "displays a zen-like message when there are no unread stories" do diff --git a/spec/fever_api/read_favicons_spec.rb b/spec/fever_api/read_favicons_spec.rb index 5b91e4b5e..93bb4efa1 100644 --- a/spec/fever_api/read_favicons_spec.rb +++ b/spec/fever_api/read_favicons_spec.rb @@ -10,7 +10,7 @@ favicons: [ { id: 0, - data: "image/gif;base64,R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" + data: "image/gif;base64,#{described_class::ICON}" } ] ) diff --git a/spec/fever_api/read_items_spec.rb b/spec/fever_api/read_items_spec.rb index ad8c167c7..fef8e3131 100644 --- a/spec/fever_api/read_items_spec.rb +++ b/spec/fever_api/read_items_spec.rb @@ -53,7 +53,8 @@ double("story", as_fever_json: { id: 5 }), double("story", as_fever_json: { id: 11 }) ] - expect(story_repository).to receive(:fetch_by_ids).with(%w(5 11)).twice.and_return(stories) + expect(story_repository) + .to receive(:fetch_by_ids).with(%w(5 11)).twice.and_return(stories) expect(subject.call("items" => nil, with_ids: "5,11")).to eq( items: [ diff --git a/spec/fever_api/sync_saved_item_ids_spec.rb b/spec/fever_api/sync_saved_item_ids_spec.rb index e9efefb64..5907df49a 100644 --- a/spec/fever_api/sync_saved_item_ids_spec.rb +++ b/spec/fever_api/sync_saved_item_ids_spec.rb @@ -13,7 +13,8 @@ it "returns a list of starred items if requested" do expect(story_repository).to receive(:all_starred).and_return(stories) - expect(subject.call("saved_item_ids" => nil)).to eq(saved_item_ids: story_ids.join(",")) + expect(subject.call("saved_item_ids" => nil)) + .to eq(saved_item_ids: story_ids.join(",")) end it "returns an empty hash otherwise" do diff --git a/spec/fever_api/sync_unread_item_ids_spec.rb b/spec/fever_api/sync_unread_item_ids_spec.rb index b7f9925f4..e7f98e9c8 100644 --- a/spec/fever_api/sync_unread_item_ids_spec.rb +++ b/spec/fever_api/sync_unread_item_ids_spec.rb @@ -13,7 +13,8 @@ it "returns a list of unread items if requested" do expect(story_repository).to receive(:unread).and_return(stories) - expect(subject.call("unread_item_ids" => nil)).to eq(unread_item_ids: story_ids.join(",")) + expect(subject.call("unread_item_ids" => nil)) + .to eq(unread_item_ids: story_ids.join(",")) end it "returns an empty hash otherwise" do diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index ebeeac78e..c2e0d2959 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -109,7 +109,8 @@ def make_request(extra_headers = {}) end it "returns stories when 'items' header is provided along with 'since_id'" do - expect(StoryRepository).to receive(:unread_since_id).with("5").and_return([story_one]) + expect(StoryRepository) + .to receive(:unread_since_id).with("5").and_return([story_one]) expect(StoryRepository).to receive(:unread).and_return([story_one, story_two]) make_request(items: nil, since_id: 5) @@ -193,7 +194,8 @@ def make_request(extra_headers = {}) end it "commands to mark story as read" do - expect(MarkAsRead).to receive(:new).with("10").and_return(double(mark_as_read: true)) + expect(MarkAsRead) + .to receive(:new).with("10").and_return(double(mark_as_read: true)) make_request(mark: "item", as: "read", id: 10) @@ -202,7 +204,8 @@ def make_request(extra_headers = {}) end it "commands to mark story as unread" do - expect(MarkAsUnread).to receive(:new).with("10").and_return(double(mark_as_unread: true)) + expect(MarkAsUnread) + .to receive(:new).with("10").and_return(double(mark_as_unread: true)) make_request(mark: "item", as: "unread", id: 10) diff --git a/spec/integration/feed_importing_spec.rb b/spec/integration/feed_importing_spec.rb index f938381cf..d1abbb14f 100644 --- a/spec/integration/feed_importing_spec.rb +++ b/spec/integration/feed_importing_spec.rb @@ -63,13 +63,13 @@ context "has been fetched before" do it "imports all new stories" do # This spec describes a scenario where the feed is reporting incorrect - # published dates for stories. - # The feed in question is feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots. - # When an article is published the published date is always set to 00:00 of - # the day the article was published. - # This specs shows that with the current behaviour (08-15-2014) Stringer - # will not detect this article, if the last time this feed was fetched is - # after 00:00 the day the article was published. + # published dates for stories. The feed in question is + # feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots. When an + # article is published the published date is always set to 00:00 of the + # day the article was published. This specs shows that with the current + # behaviour (08-15-2014) Stringer will not detect this article, if the + # last time this feed was fetched is after 00:00 the day the article + # was published. feed.last_fetched = Time.parse("2014-08-12T00:01:00Z") @server.response = sample_data("feeds/feed02_invalid_published_dates/feed.xml") diff --git a/spec/javascript/test_controller.rb b/spec/javascript/test_controller.rb index 7a13e99d8..97c62a491 100644 --- a/spec/javascript/test_controller.rb +++ b/spec/javascript/test_controller.rb @@ -23,7 +23,14 @@ def self.test_path(*chunks) private def vendor_js_files - %w(mocha.js sinon.js chai.js chai-changes.js chai-backbone.js sinon-chai.js).map do |name| + %w( + mocha.js + sinon.js + chai.js + chai-changes.js + chai-backbone.js + sinon-chai.js + ).map do |name| File.join "vendor", "js", name end end diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 92e457bd1..bb50912f0 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -11,7 +11,11 @@ end it "normalizes story urls" do - entry = double(url: "//blog.golang.org/context", title: "", content: "").as_null_object + entry = double( + url: "//blog.golang.org/context", + title: "", + content: "" + ).as_null_object expect(StoryRepository).to receive(:normalize_url).with(entry.url, feed.url) StoryRepository.add(entry, feed) diff --git a/spec/tasks/fetch_feed_spec.rb b/spec/tasks/fetch_feed_spec.rb index 36c7be165..86432b7ba 100644 --- a/spec/tasks/fetch_feed_spec.rb +++ b/spec/tasks/fetch_feed_spec.rb @@ -64,7 +64,8 @@ let(:fake_parser) { class_double(Feedjira, parse: fake_feed) } before do - allow_any_instance_of(FindNewStories).to receive(:new_stories).and_return([new_story]) + allow_any_instance_of(FindNewStories) + .to receive(:new_stories).and_return([new_story]) end it "should only add posts that are new" do @@ -118,7 +119,12 @@ output = StringIO.new logger = Logger.new(output) - FetchFeed.new(daring_fireball, parser: parser, client: client, logger: logger).fetch + FetchFeed.new( + daring_fireball, + parser: parser, + client: client, + logger: logger + ).fetch expect(output.string).to include("Something went wrong") end diff --git a/spec/tasks/fetch_feeds_spec.rb b/spec/tasks/fetch_feeds_spec.rb index 08a3325f7..8070b6988 100644 --- a/spec/tasks/fetch_feeds_spec.rb +++ b/spec/tasks/fetch_feeds_spec.rb @@ -23,7 +23,8 @@ it "finds feeds when run after a delay" do allow(pool).to receive(:process).and_yield allow(FetchFeed).to receive(:new).and_return(fetcher_one, fetcher_two) - expect(FeedRepository).to receive(:fetch_by_ids).with(feeds.map(&:id)).and_return(feeds) + expect(FeedRepository) + .to receive(:fetch_by_ids).with(feeds.map(&:id)).and_return(feeds) expect(fetcher_one).to receive(:fetch).once expect(fetcher_two).to receive(:fetch).once diff --git a/spec/tasks/remove_old_stories_spec.rb b/spec/tasks/remove_old_stories_spec.rb index e038ce10b..671e3e45f 100644 --- a/spec/tasks/remove_old_stories_spec.rb +++ b/spec/tasks/remove_old_stories_spec.rb @@ -20,7 +20,8 @@ it "should request deletion of all old stories" do allow(RemoveOldStories).to receive(:pruned_feeds) { [] } - allow(StoryRepository).to receive(:unstarred_read_stories_older_than) { stories_mock } + allow(StoryRepository) + .to receive(:unstarred_read_stories_older_than) { stories_mock } expect(stories_mock).to receive(:delete_all) From c6d4324701ab627b059060434f36375f85136d9f Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 22:00:55 -0800 Subject: [PATCH 0411/1107] RuboCop: reduce line length to 89 (#714) --- .rubocop.yml | 2 +- app/fever_api/write_mark_item.rb | 3 ++- app/repositories/story_repository.rb | 3 ++- spec/fever_api_spec.rb | 9 ++++++--- spec/repositories/story_repository_spec.rb | 3 ++- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 64a7ed716..8a93a9f45 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,7 +12,7 @@ AllCops: - 'vendor/**/*' Layout/LineLength: - Max: 90 + Max: 89 Metrics/BlockLength: Exclude: diff --git a/app/fever_api/write_mark_item.rb b/app/fever_api/write_mark_item.rb index 28346bc08..7a4a563b8 100644 --- a/app/fever_api/write_mark_item.rb +++ b/app/fever_api/write_mark_item.rb @@ -9,7 +9,8 @@ def initialize(options = {}) @read_marker_class = options.fetch(:read_marker_class) { MarkAsRead } @unread_marker_class = options.fetch(:unread_marker_class) { MarkAsUnread } @starred_marker_class = options.fetch(:starred_marker_class) { MarkAsStarred } - @unstarred_marker_class = options.fetch(:unstarred_marker_class) { MarkAsUnstarred } + @unstarred_marker_class = + options.fetch(:unstarred_marker_class) { MarkAsUnstarred } end def call(params = {}) diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index 941d2cd3f..f111b9151 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -35,7 +35,8 @@ def self.fetch_unread_by_timestamp(timestamp) end def self.fetch_unread_by_timestamp_and_group(timestamp, group_id) - fetch_unread_by_timestamp(timestamp).joins(:feed).where(feeds: { group_id: group_id }) + fetch_unread_by_timestamp(timestamp) + .joins(:feed).where(feeds: { group_id: group_id }) end def self.fetch_unread_for_feed_by_timestamp(feed_id, timestamp) diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index c2e0d2959..6746e67f3 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -67,7 +67,8 @@ def make_request(extra_headers = {}) it "returns groups and feeds by groups when 'groups' header is provided" do allow(GroupRepository).to receive(:list).and_return([group]) - allow(FeedRepository).to receive_message_chain(:in_group, :order).and_return([feed]) + allow(FeedRepository) + .to receive_message_chain(:in_group, :order).and_return([feed]) make_request(groups: nil) @@ -81,7 +82,8 @@ def make_request(extra_headers = {}) it "returns feeds and feeds by groups when 'feeds' header is provided" do allow(FeedRepository).to receive(:list).and_return([feed]) - allow(FeedRepository).to receive_message_chain(:in_group, :order).and_return([feed]) + allow(FeedRepository) + .to receive_message_chain(:in_group, :order).and_return([feed]) make_request(feeds: nil) @@ -124,7 +126,8 @@ def make_request(extra_headers = {}) end it "returns stories when 'items' header is provided without 'since_id'" do - expect(StoryRepository).to receive(:unread).twice.and_return([story_one, story_two]) + expect(StoryRepository) + .to receive(:unread).twice.and_return([story_one, story_two]) make_request(items: nil) diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index bb50912f0..ca83cfb3d 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -31,7 +31,8 @@ end it "deletes script tags from titles" do - entry = double(title: "n", content: "").as_null_object + entry = double(title: "n", content: "") + .as_null_object allow(StoryRepository).to receive(:normalize_url) expect(Story).to receive(:create).with(hash_including(title: "n")) From 0cd1646f6b4526bf90878332fc72e1be68e4fa2f Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 22:17:32 -0800 Subject: [PATCH 0412/1107] RuboCop: reduce line length to 88 (#715) --- .rubocop.yml | 2 +- app/fever_api/read_feeds_groups.rb | 3 ++- spec/commands/find_new_stories_spec.rb | 12 ++++++++++-- spec/fever_api/read_feeds_spec.rb | 6 +++++- spec/tasks/fetch_feed_spec.rb | 14 ++++++++++++-- spec/tasks/remove_old_stories_spec.rb | 5 ++++- 6 files changed, 34 insertions(+), 8 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 8a93a9f45..1f497fe7d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,7 +12,7 @@ AllCops: - 'vendor/**/*' Layout/LineLength: - Max: 89 + Max: 88 Metrics/BlockLength: Exclude: diff --git a/app/fever_api/read_feeds_groups.rb b/app/fever_api/read_feeds_groups.rb index 6e6513b47..13cec3b93 100644 --- a/app/fever_api/read_feeds_groups.rb +++ b/app/fever_api/read_feeds_groups.rb @@ -17,7 +17,8 @@ def call(params = {}) private def feeds_groups - grouped_feeds = @feed_repository.in_group.order("LOWER(name)").group_by(&:group_id) + grouped_feeds = + @feed_repository.in_group.order("LOWER(name)").group_by(&:group_id) grouped_feeds.map do |group_id, feeds| { group_id: group_id, diff --git a/spec/commands/find_new_stories_spec.rb b/spec/commands/find_new_stories_spec.rb index 41cce2054..0fdccf4a2 100644 --- a/spec/commands/find_new_stories_spec.rb +++ b/spec/commands/find_new_stories_spec.rb @@ -39,7 +39,12 @@ old_story = double(published: nil, id: "old-story") feed = double(last_modified: nil, entries: [new_story, old_story]) - result = FindNewStories.new(feed, 1, Time.new(2013, 1, 3), "old-story").new_stories + result = FindNewStories.new( + feed, + 1, + Time.new(2013, 1, 3), + "old-story" + ).new_stories expect(result).to eq [new_story] end @@ -54,7 +59,10 @@ double(published: 4.days.ago, id: "new-story") ] - feed = double(last_modified: nil, entries: new_stories + stories_older_than_3_days) + feed = double( + last_modified: nil, + entries: new_stories + stories_older_than_3_days + ) result = FindNewStories.new(feed, 1, nil, nil).new_stories expect(result).not_to include(stories_older_than_3_days) diff --git a/spec/fever_api/read_feeds_spec.rb b/spec/fever_api/read_feeds_spec.rb index e09fa0ab7..3b0c36998 100644 --- a/spec/fever_api/read_feeds_spec.rb +++ b/spec/fever_api/read_feeds_spec.rb @@ -4,7 +4,11 @@ describe FeverAPI::ReadFeeds do let(:feed_ids) { [5, 7, 11] } - let(:feeds) { feed_ids.map { |id| double("feed", id: id, as_fever_json: { id: id }) } } + let(:feeds) do + feed_ids.map do |id| + double("feed", id: id, as_fever_json: { id: id }) + end + end let(:feed_repository) { double("repo") } subject do diff --git a/spec/tasks/fetch_feed_spec.rb b/spec/tasks/fetch_feed_spec.rb index 86432b7ba..73950908b 100644 --- a/spec/tasks/fetch_feed_spec.rb +++ b/spec/tasks/fetch_feed_spec.rb @@ -25,7 +25,12 @@ expect(StoryRepository).not_to receive(:add) - FetchFeed.new(daring_fireball, parser: parser, client: client, logger: nil).fetch + FetchFeed.new( + daring_fireball, + parser: parser, + client: client, + logger: nil + ).fetch end it "logs a message" do @@ -110,7 +115,12 @@ expect(FeedRepository).to receive(:set_status) .with(:red, daring_fireball) - FetchFeed.new(daring_fireball, parser: parser, client: client, logger: nil).fetch + FetchFeed.new( + daring_fireball, + parser: parser, + client: client, + logger: nil + ).fetch end it "outputs a message when things go wrong" do diff --git a/spec/tasks/remove_old_stories_spec.rb b/spec/tasks/remove_old_stories_spec.rb index 671e3e45f..f9ba2e35d 100644 --- a/spec/tasks/remove_old_stories_spec.rb +++ b/spec/tasks/remove_old_stories_spec.rb @@ -45,7 +45,10 @@ allow(RemoveOldStories).to receive(:pruned_feeds) { feeds } allow(RemoveOldStories).to receive(:old_stories) { stories_mock } - expect(FeedRepository).to receive(:update_last_fetched).with(feeds.first, anything) + expect(FeedRepository).to receive(:update_last_fetched).with( + feeds.first, + anything + ) expect(FeedRepository).to receive(:update_last_fetched).with(feeds.last, anything) RemoveOldStories.remove!(13) From 03c7f4159694d4668ed493c20095cf70dfce6df8 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 22:27:26 -0800 Subject: [PATCH 0413/1107] RuboCop: reduce line length to 87 (#716) --- .rubocop.yml | 2 +- app.rb | 3 ++- app/tasks/fetch_feed.rb | 7 ++++++- ...0805113712_update_stories_unique_constraints.rb | 5 ++++- spec/controllers/exports_controller_spec.rb | 14 +++++++++++--- spec/fever_api/read_feeds_groups_spec.rb | 6 ++++-- spec/tasks/remove_old_stories_spec.rb | 9 ++++----- 7 files changed, 32 insertions(+), 14 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 1f497fe7d..98d051a3c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,7 +12,7 @@ AllCops: - 'vendor/**/*' Layout/LineLength: - Max: 88 + Max: 87 Metrics/BlockLength: Exclude: diff --git a/app.rb b/app.rb index 8b4557e5c..4baa6c30a 100644 --- a/app.rb +++ b/app.rb @@ -31,7 +31,8 @@ def self.application end end -I18n.load_path += Dir[File.join(File.dirname(__FILE__), "config/locales", "*.yml").to_s] +I18n.load_path += + Dir[File.join(File.dirname(__FILE__), "config/locales", "*.yml").to_s] I18n.config.enforce_available_locales = false Time.zone = ENV.fetch("TZ", "UTC") diff --git a/app/tasks/fetch_feed.rb b/app/tasks/fetch_feed.rb index c683823e3..49d982421 100644 --- a/app/tasks/fetch_feed.rb +++ b/app/tasks/fetch_feed.rb @@ -49,7 +49,12 @@ def feed_modified(raw_feed) end def new_entries_from(raw_feed) - finder = FindNewStories.new(raw_feed, @feed.id, @feed.last_fetched, latest_entry_id) + finder = FindNewStories.new( + raw_feed, + @feed.id, + @feed.last_fetched, + latest_entry_id + ) finder.new_stories end diff --git a/db/migrate/20130805113712_update_stories_unique_constraints.rb b/db/migrate/20130805113712_update_stories_unique_constraints.rb index 53ec20b71..b18812948 100644 --- a/db/migrate/20130805113712_update_stories_unique_constraints.rb +++ b/db/migrate/20130805113712_update_stories_unique_constraints.rb @@ -6,6 +6,9 @@ def up def down remove_index :stories, [:entry_id, :feed_id] - add_index :stories, [:permalink, :feed_id], unique: true, length: { permalink: 767 } + add_index :stories, + [:permalink, :feed_id], + unique: true, + length: { permalink: 767 } end end diff --git a/spec/controllers/exports_controller_spec.rb b/spec/controllers/exports_controller_spec.rb index f4f0016be..44cad0ed2 100644 --- a/spec/controllers/exports_controller_spec.rb +++ b/spec/controllers/exports_controller_spec.rb @@ -15,14 +15,22 @@ expect(last_response.body).to eq some_xml end - it "responds with OPML headers" do + it "responds with xml content type" do expect_any_instance_of(ExportToOpml).to receive(:to_xml).and_return(some_xml) get "/feeds/export" expect(last_response.header["Content-Type"]).to include "application/xml" - expect(last_response.header["Content-Disposition"]) - .to eq("attachment; filename=\"stringer.opml\"; filename*=UTF-8''stringer.opml") + end + + it "responds with disposition attachment" do + expect_any_instance_of(ExportToOpml).to receive(:to_xml).and_return(some_xml) + + get "/feeds/export" + + expected_disposition = + "attachment; filename=\"stringer.opml\"; filename*=UTF-8''stringer.opml" + expect(last_response.header["Content-Disposition"]).to eq(expected_disposition) end end end diff --git a/spec/fever_api/read_feeds_groups_spec.rb b/spec/fever_api/read_feeds_groups_spec.rb index 7da9707de..81204fc3a 100644 --- a/spec/fever_api/read_feeds_groups_spec.rb +++ b/spec/fever_api/read_feeds_groups_spec.rb @@ -12,7 +12,8 @@ end it "returns a list of groups requested through feeds" do - allow(feed_repository).to receive_message_chain(:in_group, :order).and_return(feeds) + allow(feed_repository) + .to receive_message_chain(:in_group, :order).and_return(feeds) expect(subject.call("feeds" => nil)).to eq( feeds_groups: [ @@ -25,7 +26,8 @@ end it "returns a list of groups requested through groups" do - allow(feed_repository).to receive_message_chain(:in_group, :order).and_return(feeds) + allow(feed_repository) + .to receive_message_chain(:in_group, :order).and_return(feeds) expect(subject.call("groups" => nil)).to eq( feeds_groups: [ diff --git a/spec/tasks/remove_old_stories_spec.rb b/spec/tasks/remove_old_stories_spec.rb index f9ba2e35d..cca650096 100644 --- a/spec/tasks/remove_old_stories_spec.rb +++ b/spec/tasks/remove_old_stories_spec.rb @@ -45,11 +45,10 @@ allow(RemoveOldStories).to receive(:pruned_feeds) { feeds } allow(RemoveOldStories).to receive(:old_stories) { stories_mock } - expect(FeedRepository).to receive(:update_last_fetched).with( - feeds.first, - anything - ) - expect(FeedRepository).to receive(:update_last_fetched).with(feeds.last, anything) + expect(FeedRepository) + .to receive(:update_last_fetched).with(feeds.first, anything) + expect(FeedRepository) + .to receive(:update_last_fetched).with(feeds.last, anything) RemoveOldStories.remove!(13) end From 97f4008934bf9967dbe3ec901ade7ad244b7dc22 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 22:32:21 -0800 Subject: [PATCH 0414/1107] RuboCop: reduce line length to 86 (#717) --- .rubocop.yml | 2 +- app/tasks/fetch_feed.rb | 7 ++++++- ...30805113712_update_stories_unique_constraints.rb | 5 ++++- spec/commands/find_new_stories_spec.rb | 3 ++- spec/controllers/feeds_controller_spec.rb | 5 +++-- spec/factories/feed_factory.rb | 13 +++++++------ spec/integration/feed_importing_spec.rb | 3 ++- 7 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 98d051a3c..562ce3a6e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,7 +12,7 @@ AllCops: - 'vendor/**/*' Layout/LineLength: - Max: 87 + Max: 86 Metrics/BlockLength: Exclude: diff --git a/app/tasks/fetch_feed.rb b/app/tasks/fetch_feed.rb index 49d982421..aed06a496 100644 --- a/app/tasks/fetch_feed.rb +++ b/app/tasks/fetch_feed.rb @@ -6,7 +6,12 @@ require_relative "../commands/feeds/find_new_stories" class FetchFeed - def initialize(feed, parser: Feedjira, client: HTTParty, logger: Logger.new($stdout)) + def initialize( + feed, + parser: Feedjira, + client: HTTParty, + logger: Logger.new($stdout) + ) @feed = feed @parser = parser @client = client diff --git a/db/migrate/20130805113712_update_stories_unique_constraints.rb b/db/migrate/20130805113712_update_stories_unique_constraints.rb index b18812948..2c320babb 100644 --- a/db/migrate/20130805113712_update_stories_unique_constraints.rb +++ b/db/migrate/20130805113712_update_stories_unique_constraints.rb @@ -1,7 +1,10 @@ class UpdateStoriesUniqueConstraints < ActiveRecord::Migration[4.2] def up remove_index :stories, [:permalink, :feed_id] - add_index :stories, [:entry_id, :feed_id], unique: true, length: { permalink: 767 } + add_index :stories, + [:entry_id, :feed_id], + unique: true, + length: { permalink: 767 } end def down diff --git a/spec/commands/find_new_stories_spec.rb b/spec/commands/find_new_stories_spec.rb index 0fdccf4a2..0d5edbb85 100644 --- a/spec/commands/find_new_stories_spec.rb +++ b/spec/commands/find_new_stories_spec.rb @@ -27,7 +27,8 @@ feed = double(entries: [story1, story2]) allow(StoryRepository).to receive(:exists?).with("story1", 1).and_return(true) - allow(StoryRepository).to receive(:exists?).with("story2", 1).and_return(false) + allow(StoryRepository) + .to receive(:exists?).with("story2", 1).and_return(false) result = FindNewStories.new(feed, 1, Time.new(2013, 1, 2)).new_stories expect(result).to eq [story2] diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index 80290eb8d..f8771ae3a 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -55,10 +55,11 @@ def params(feed, **overrides) describe "PUT /feeds/:feed_id" do it "updates a feed given the id" do - feed = FeedFactory.build(url: "example.com/atom") + feed = FeedFactory.build(url: "example.com/atom", id: "12", group_id: nil) mock_feed(feed, "Test", "example.com/feed") - put "/feeds/123", feed_id: "123", feed_name: "Test", feed_url: "example.com/feed" + feed_url = "example.com/feed" + put "/feeds/123", **params(feed, feed_name: "Test", feed_url:) expect(last_response).to be_redirect end diff --git a/spec/factories/feed_factory.rb b/spec/factories/feed_factory.rb index 3c64ab1ef..3355d2374 100644 --- a/spec/factories/feed_factory.rb +++ b/spec/factories/feed_factory.rb @@ -16,12 +16,13 @@ def as_fever_json def self.build(params = {}) FakeFeed.new( id: rand(100), - group_id: params[:group_id] || rand(100), - name: params[:name] || Faker::Name.name + " on Software", # rubocop:disable Style/StringConcatenation - url: params[:url] || Faker::Internet.url, - last_fetched: params[:last_fetched] || Time.now, - stories: params[:stories] || [], - unread_stories: [] + group_id: rand(100), + name: Faker::Name.name + " on Software", # rubocop:disable Style/StringConcatenation + url: Faker::Internet.url, + last_fetched: Time.now, + stories: [], + unread_stories: [], + **params ) end end diff --git a/spec/integration/feed_importing_spec.rb b/spec/integration/feed_importing_spec.rb index d1abbb14f..636f13df9 100644 --- a/spec/integration/feed_importing_spec.rb +++ b/spec/integration/feed_importing_spec.rb @@ -72,7 +72,8 @@ # was published. feed.last_fetched = Time.parse("2014-08-12T00:01:00Z") - @server.response = sample_data("feeds/feed02_invalid_published_dates/feed.xml") + @server.response = + sample_data("feeds/feed02_invalid_published_dates/feed.xml") expect { fetch_feed(feed) }.to change { feed.stories.count }.by(1) end From fadd0b07374c82b7980b03dd53d71948b94e5b70 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 22:37:54 -0800 Subject: [PATCH 0415/1107] RuboCop: reduce line length to 85 (#719) --- .rubocop.yml | 2 +- spec/commands/find_new_stories_spec.rb | 3 ++- spec/fever_api/write_mark_group_spec.rb | 3 ++- spec/repositories/story_repository_spec.rb | 10 ++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 562ce3a6e..37b794ec3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,7 +12,7 @@ AllCops: - 'vendor/**/*' Layout/LineLength: - Max: 86 + Max: 85 Metrics/BlockLength: Exclude: diff --git a/spec/commands/find_new_stories_spec.rb b/spec/commands/find_new_stories_spec.rb index 0d5edbb85..c0992e592 100644 --- a/spec/commands/find_new_stories_spec.rb +++ b/spec/commands/find_new_stories_spec.rb @@ -26,7 +26,8 @@ story2 = double(published: nil, id: "story2") feed = double(entries: [story1, story2]) - allow(StoryRepository).to receive(:exists?).with("story1", 1).and_return(true) + allow(StoryRepository) + .to receive(:exists?).with("story1", 1).and_return(true) allow(StoryRepository) .to receive(:exists?).with("story2", 1).and_return(false) diff --git a/spec/fever_api/write_mark_group_spec.rb b/spec/fever_api/write_mark_group_spec.rb index 1262f048c..91044ea56 100644 --- a/spec/fever_api/write_mark_group_spec.rb +++ b/spec/fever_api/write_mark_group_spec.rb @@ -11,7 +11,8 @@ end it "instantiates a group marker and calls mark_group_as_read if requested" do - expect(marker_class).to receive(:new).with(5, 1234567890).and_return(group_marker) + expect(marker_class) + .to receive(:new).with(5, 1234567890).and_return(group_marker) expect(group_marker).to receive(:mark_group_as_read) expect(subject.call(mark: "group", id: 5, before: 1234567890)).to eq({}) end diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index ca83cfb3d..b2d0c7d15 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -481,13 +481,11 @@ end it "ignores URL expansion if entry url is nil" do - entry = double( - url: nil, - content: nil, - summary: "Page" - ) + entry = + double(url: nil, content: nil, summary: "Page") - expect(StoryRepository.extract_content(entry)).to eq "Page" + expect(StoryRepository.extract_content(entry)) + .to eq "Page" end end end From 8468b690c3988e84f2dcc691c9cf26a0705a21a9 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 22:46:44 -0800 Subject: [PATCH 0416/1107] RuboCop: reduce line length to 84 (#720) --- .rubocop.yml | 2 +- app/commands/feeds/import_from_opml.rb | 9 +++++---- app/repositories/story_repository.rb | 4 ++-- spec/controllers/exports_controller_spec.rb | 4 ++-- spec/fever_api/write_mark_feed_spec.rb | 3 ++- spec/fever_api/write_mark_item_spec.rb | 8 ++++---- spec/fever_api_spec.rb | 4 ++-- spec/tasks/fetch_feed_spec.rb | 17 +++++++++++++---- 8 files changed, 31 insertions(+), 20 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 37b794ec3..38514ceb2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,7 +12,7 @@ AllCops: - 'vendor/**/*' Layout/LineLength: - Max: 85 + Max: 84 Metrics/BlockLength: Exclude: diff --git a/app/commands/feeds/import_from_opml.rb b/app/commands/feeds/import_from_opml.rb index b0133e20e..a0d911cc8 100644 --- a/app/commands/feeds/import_from_opml.rb +++ b/app/commands/feeds/import_from_opml.rb @@ -9,10 +9,11 @@ class << self def import(opml_contents) feeds_with_groups = OpmlParser.new.parse_feeds(opml_contents) - # It considers a situation when feeds are already imported without groups, - # so it's possible to re-import the same subscriptions.xml just to set group_id - # for existing feeds. Feeds without groups are in 'Ungrouped' group, we don't - # create such group and create such feeds with group_id = nil. + # It considers a situation when feeds are already imported without + # groups, so it's possible to re-import the same subscriptions.xml just + # to set group_id for existing feeds. Feeds without groups are in + # 'Ungrouped' group, we don't create such group and create such feeds + # with group_id = nil. feeds_with_groups.each do |group_name, parsed_feeds| unless group_name == "Ungrouped" group = Group.where(name: group_name).first_or_create diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index f111b9151..b6d8a7e47 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -90,9 +90,9 @@ def self.read_count end def self.extract_url(entry, feed) - return entry.enclosure_url if entry.url.nil? && entry.respond_to?(:enclosure_url) + return normalize_url(entry.url, feed.url) if entry.url.present? - normalize_url(entry.url, feed.url) unless entry.url.nil? + entry.enclosure_url if entry.respond_to?(:enclosure_url) end def self.extract_content(entry) diff --git a/spec/controllers/exports_controller_spec.rb b/spec/controllers/exports_controller_spec.rb index 44cad0ed2..e6539c7d8 100644 --- a/spec/controllers/exports_controller_spec.rb +++ b/spec/controllers/exports_controller_spec.rb @@ -28,9 +28,9 @@ get "/feeds/export" - expected_disposition = + expected = "attachment; filename=\"stringer.opml\"; filename*=UTF-8''stringer.opml" - expect(last_response.header["Content-Disposition"]).to eq(expected_disposition) + expect(last_response.header["Content-Disposition"]).to eq(expected) end end end diff --git a/spec/fever_api/write_mark_feed_spec.rb b/spec/fever_api/write_mark_feed_spec.rb index 793112034..65efde1d8 100644 --- a/spec/fever_api/write_mark_feed_spec.rb +++ b/spec/fever_api/write_mark_feed_spec.rb @@ -11,7 +11,8 @@ end it "instantiates a feed marker and calls mark_feed_as_read if requested" do - expect(marker_class).to receive(:new).with(5, 1234567890).and_return(feed_marker) + expect(marker_class) + .to receive(:new).with(5, 1234567890).and_return(feed_marker) expect(feed_marker).to receive(:mark_feed_as_read) expect(subject.call(mark: "feed", id: 5, before: 1234567890)).to eq({}) end diff --git a/spec/fever_api/write_mark_item_spec.rb b/spec/fever_api/write_mark_item_spec.rb index ef70c519f..483d2eba9 100644 --- a/spec/fever_api/write_mark_item_spec.rb +++ b/spec/fever_api/write_mark_item_spec.rb @@ -11,7 +11,7 @@ FeverAPI::WriteMarkItem.new(read_marker_class: marker_class) end - it "instantiates an item marker and calls mark_item_as_read if requested" do + it "calls mark_item_as_read if requested" do expect(marker_class).to receive(:new).with(5).and_return(item_marker) expect(item_marker).to receive(:mark_as_read) expect(subject.call(mark: "item", as: "read", id: 5)).to eq({}) @@ -23,7 +23,7 @@ FeverAPI::WriteMarkItem.new(unread_marker_class: marker_class) end - it "instantiates an item marker and calls mark_item_as_unread if requested" do + it "calls mark_item_as_unread if requested" do expect(marker_class).to receive(:new).with(5).and_return(item_marker) expect(item_marker).to receive(:mark_as_unread) expect(subject.call(mark: "item", as: "unread", id: 5)).to eq({}) @@ -35,7 +35,7 @@ FeverAPI::WriteMarkItem.new(starred_marker_class: marker_class) end - it "instantiates an item marker and calls mark_item_as_starred if requested" do + it "calls mark_item_as_starred if requested" do expect(marker_class).to receive(:new).with(5).and_return(item_marker) expect(item_marker).to receive(:mark_as_starred) expect(subject.call(mark: "item", as: "saved", id: 5)).to eq({}) @@ -47,7 +47,7 @@ FeverAPI::WriteMarkItem.new(unstarred_marker_class: marker_class) end - it "instantiates an item marker and calls mark_item_as_unstarred if requested" do + it "calls marks_item_as_unstarred if requested" do expect(marker_class).to receive(:new).with(5).and_return(item_marker) expect(item_marker).to receive(:mark_as_unstarred) expect(subject.call(mark: "item", as: "unsaved", id: 5)).to eq({}) diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index 6746e67f3..3071a873a 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -110,7 +110,7 @@ def make_request(extra_headers = {}) ) end - it "returns stories when 'items' header is provided along with 'since_id'" do + it "returns stories when 'items' and 'since_id'" do expect(StoryRepository) .to receive(:unread_since_id).with("5").and_return([story_one]) expect(StoryRepository).to receive(:unread).and_return([story_one, story_two]) @@ -139,7 +139,7 @@ def make_request(extra_headers = {}) ) end - it "returns stories ids when 'items' header is provided along with 'with_ids'" do + it "returns stories ids when 'items' and 'with_ids'" do expect(StoryRepository) .to receive(:fetch_by_ids).twice.with(["5"]).and_return([story_one]) diff --git a/spec/tasks/fetch_feed_spec.rb b/spec/tasks/fetch_feed_spec.rb index 73950908b..6d99204f0 100644 --- a/spec/tasks/fetch_feed_spec.rb +++ b/spec/tasks/fetch_feed_spec.rb @@ -39,7 +39,12 @@ output = StringIO.new logger = Logger.new(output) - FetchFeed.new(daring_fireball, parser: parser, client: client, logger:).fetch + FetchFeed.new( + daring_fireball, + parser: parser, + client: client, + logger: + ).fetch expect(output.string).to include("has not been modified") end @@ -51,7 +56,8 @@ client = class_spy(HTTParty) parser = class_double(Feedjira, parse: fake_feed) - allow_any_instance_of(FindNewStories).to receive(:new_stories).and_return([]) + allow_any_instance_of(FindNewStories) + .to receive(:new_stories).and_return([]) expect(StoryRepository).not_to receive(:add) @@ -64,7 +70,9 @@ let(:new_story) { double } let(:old_story) { double } - let(:fake_feed) { double(last_modified: now, entries: [new_story, old_story]) } + let(:fake_feed) do + double(last_modified: now, entries: [new_story, old_story]) + end let(:fake_client) { class_spy(HTTParty) } let(:fake_parser) { class_double(Feedjira, parse: fake_feed) } @@ -75,7 +83,8 @@ it "should only add posts that are new" do expect(StoryRepository).to receive(:add).with(new_story, daring_fireball) - expect(StoryRepository).not_to receive(:add).with(old_story, daring_fireball) + expect(StoryRepository) + .not_to receive(:add).with(old_story, daring_fireball) FetchFeed.new( daring_fireball, From 020975bf34ff789fe059744d9b4ae632765157a6 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 22:56:57 -0800 Subject: [PATCH 0417/1107] RuboCop: reduce line length to 83 (#721) --- .rubocop.yml | 2 +- app/fever_api/write_mark_item.rb | 3 ++- spec/controllers/debug_controller_spec.rb | 3 ++- spec/fever_api_spec.rb | 14 ++++++-------- spec/helpers/url_helpers_spec.rb | 15 ++++++++------- spec/integration/feed_importing_spec.rb | 3 ++- 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 38514ceb2..2726a0ac7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,7 +12,7 @@ AllCops: - 'vendor/**/*' Layout/LineLength: - Max: 84 + Max: 83 Metrics/BlockLength: Exclude: diff --git a/app/fever_api/write_mark_item.rb b/app/fever_api/write_mark_item.rb index 7a4a563b8..0ac82c1b4 100644 --- a/app/fever_api/write_mark_item.rb +++ b/app/fever_api/write_mark_item.rb @@ -8,7 +8,8 @@ class WriteMarkItem def initialize(options = {}) @read_marker_class = options.fetch(:read_marker_class) { MarkAsRead } @unread_marker_class = options.fetch(:unread_marker_class) { MarkAsUnread } - @starred_marker_class = options.fetch(:starred_marker_class) { MarkAsStarred } + @starred_marker_class = + options.fetch(:starred_marker_class) { MarkAsStarred } @unstarred_marker_class = options.fetch(:unstarred_marker_class) { MarkAsUnstarred } end diff --git a/spec/controllers/debug_controller_spec.rb b/spec/controllers/debug_controller_spec.rb index 937ea10d5..d5cae8cbc 100644 --- a/spec/controllers/debug_controller_spec.rb +++ b/spec/controllers/debug_controller_spec.rb @@ -15,7 +15,8 @@ .to receive(:pending_migrations) .and_return(["Migration B - 2", "Migration C - 3"]) migration_status = double "MigrationStatus" - allow(migration_status).to receive(:new).and_return(migration_status_instance) + allow(migration_status) + .to receive(:new).and_return(migration_status_instance) stub_const("MigrationStatus", migration_status) end diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index 3071a873a..4c3bef019 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -113,7 +113,8 @@ def make_request(extra_headers = {}) it "returns stories when 'items' and 'since_id'" do expect(StoryRepository) .to receive(:unread_since_id).with("5").and_return([story_one]) - expect(StoryRepository).to receive(:unread).and_return([story_one, story_two]) + expect(StoryRepository) + .to receive(:unread).and_return([story_one, story_two]) make_request(items: nil, since_id: 5) @@ -162,7 +163,8 @@ def make_request(extra_headers = {}) end it "returns unread items ids when 'unread_item_ids' header is provided" do - expect(StoryRepository).to receive(:unread).and_return([story_one, story_two]) + expect(StoryRepository) + .to receive(:unread).and_return([story_one, story_two]) make_request(unread_item_ids: nil) @@ -174,12 +176,8 @@ def make_request(extra_headers = {}) end it "returns starred items when 'saved_item_ids' header is provided" do - expect(Story).to receive(:where).with(is_starred: true).and_return( - [ - story_one, - story_two - ] - ) + expect(Story).to receive(:where).with(is_starred: true) + .and_return([story_one, story_two]) make_request(saved_item_ids: nil) diff --git a/spec/helpers/url_helpers_spec.rb b/spec/helpers/url_helpers_spec.rb index 27a044ed1..e49fc7ebe 100644 --- a/spec/helpers/url_helpers_spec.rb +++ b/spec/helpers/url_helpers_spec.rb @@ -60,13 +60,14 @@ class Helper # rubocop:disable Lint/ConstantDefinitionInBlock end it "leaves the url as-is if it cannot be parsed" do - weird_url = "https://github.com/aphyr/jepsen/blob/" \ - "1403f2d6e61c595bafede0d404fd4a893371c036/" \ - "elasticsearch/src/jepsen/system/elasticsearch.clj#" \ - "L161-L226.%20Then%20we'll%20write%20a%20%5Bregister%20test%5D(" \ - "https://github.com/aphyr/jepsen/blob/" \ - "1403f2d6e61c595bafede0d404fd4a893371c036/" \ - "elasticsearch/test/jepsen/system/elasticsearch_test.clj#L18-L50)" + weird_url = + "https://github.com/aphyr/jepsen/blob/" \ + "1403f2d6e61c595bafede0d404fd4a893371c036/" \ + "elasticsearch/src/jepsen/system/elasticsearch.clj#" \ + "L161-L226.%20Then%20we'll%20write%20a%20%5Bregister%20test%5D(" \ + "https://github.com/aphyr/jepsen/blob/" \ + "1403f2d6e61c595bafede0d404fd4a893371c036/" \ + "elasticsearch/test/jepsen/system/elasticsearch_test.clj#L18-L50)" content = "" diff --git a/spec/integration/feed_importing_spec.rb b/spec/integration/feed_importing_spec.rb index 636f13df9..f7d99df80 100644 --- a/spec/integration/feed_importing_spec.rb +++ b/spec/integration/feed_importing_spec.rb @@ -48,7 +48,8 @@ context "new entries" do it "creates new stories" do - @server.response = sample_data("feeds/feed01_valid_feed/feed_updated.xml") + @server.response = + sample_data("feeds/feed01_valid_feed/feed_updated.xml") expect { fetch_feed(feed) }.to change(feed.stories, :count).by(1) end end From 8d2391b66fa722c3ae1d0c5778b27ea68d798aac Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 23:01:06 -0800 Subject: [PATCH 0418/1107] RuboCop: reduce line length to 82 (#722) --- .rubocop.yml | 2 +- app/tasks/change_password.rb | 2 +- spec/commands/feeds/add_new_feed_spec.rb | 5 ++++- spec/controllers/exports_controller_spec.rb | 11 ++++++++--- spec/controllers/feeds_controller_spec.rb | 10 ++++++++-- spec/repositories/feed_repository_spec.rb | 2 +- 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 2726a0ac7..ef47d19d2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,7 +12,7 @@ AllCops: - 'vendor/**/*' Layout/LineLength: - Max: 83 + Max: 82 Metrics/BlockLength: Exclude: diff --git a/app/tasks/change_password.rb b/app/tasks/change_password.rb index 585ba3f1d..a038f5ec5 100644 --- a/app/tasks/change_password.rb +++ b/app/tasks/change_password.rb @@ -11,7 +11,7 @@ def initialize(command = ChangeUserPassword.new, output: $stdout, input: $stdin) def change_password while (password = ask_password) != ask_confirmation - @output.puts "The confirmation doesn't match the password. Please try again." + @output.puts I18n.t("first_run.flash.passwords_dont_match") end @command.change_user_password(password) end diff --git a/spec/commands/feeds/add_new_feed_spec.rb b/spec/commands/feeds/add_new_feed_spec.rb index 54526730d..3cc19ee65 100644 --- a/spec/commands/feeds/add_new_feed_spec.rb +++ b/spec/commands/feeds/add_new_feed_spec.rb @@ -30,7 +30,10 @@ context "title includes a script tag" do let(:feed_result) do - double(title: "foobar", feed_url: feed.url) + double( + title: "foobar", + feed_url: feed.url + ) end it "deletes the script tag from the title" do diff --git a/spec/controllers/exports_controller_spec.rb b/spec/controllers/exports_controller_spec.rb index e6539c7d8..f94891239 100644 --- a/spec/controllers/exports_controller_spec.rb +++ b/spec/controllers/exports_controller_spec.rb @@ -7,8 +7,13 @@ let(:some_xml) { "some dummy opml" } before { allow(Feed).to receive(:all) } + def mock_export + expect_any_instance_of(ExportToOpml) + .to receive(:to_xml).and_return(some_xml) + end + it "returns an OPML file" do - expect_any_instance_of(ExportToOpml).to receive(:to_xml).and_return(some_xml) + mock_export get "/feeds/export" @@ -16,7 +21,7 @@ end it "responds with xml content type" do - expect_any_instance_of(ExportToOpml).to receive(:to_xml).and_return(some_xml) + mock_export get "/feeds/export" @@ -24,7 +29,7 @@ end it "responds with disposition attachment" do - expect_any_instance_of(ExportToOpml).to receive(:to_xml).and_return(some_xml) + mock_export get "/feeds/export" diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index f8771ae3a..e070f07c3 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -40,7 +40,12 @@ def mock_feed(feed, name, url, group_id = nil) expect(FeedRepository).to receive(:fetch).with("123").and_return(feed) - expect(FeedRepository).to receive(:update_feed).with(feed, name, url, group_id) + expect(FeedRepository).to receive(:update_feed).with( + feed, + name, + url, + group_id + ) end def params(feed, **overrides) @@ -126,7 +131,8 @@ def params(feed, **overrides) let(:invalid_feed) { double(valid?: false) } it "adds the feed and queues it to be fetched" do - expect(AddNewFeed).to receive(:add).with(feed_url).and_return(invalid_feed) + expect(AddNewFeed) + .to receive(:add).with(feed_url).and_return(invalid_feed) post("/feeds", feed_url:) diff --git a/spec/repositories/feed_repository_spec.rb b/spec/repositories/feed_repository_spec.rb index 75ab32846..e67bcfc03 100644 --- a/spec/repositories/feed_repository_spec.rb +++ b/spec/repositories/feed_repository_spec.rb @@ -68,7 +68,7 @@ expect(feed.last_fetched).to eq timestamp end - it "doesn't update if timestamp is nil (feed does not report last modified)" do + it "doesn't update if timestamp is nil" do feed = Feed.new(last_fetched: timestamp) FeedRepository.update_last_fetched(feed, nil) From 0a3ed12562f22d114f9a7623d37782a0b887324f Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 23:05:35 -0800 Subject: [PATCH 0419/1107] RuboCop: reduce line length to 81 (#723) --- .rubocop.yml | 2 +- app/tasks/change_password.rb | 6 +++++- config/puma.rb | 3 ++- spec/commands/users/create_user_spec.rb | 2 +- spec/fever_api/read_groups_spec.rb | 4 +++- spec/repositories/story_repository_spec.rb | 9 ++++++--- spec/tasks/remove_old_stories_spec.rb | 3 ++- 7 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index ef47d19d2..03f17941d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,7 +12,7 @@ AllCops: - 'vendor/**/*' Layout/LineLength: - Max: 82 + Max: 81 Metrics/BlockLength: Exclude: diff --git a/app/tasks/change_password.rb b/app/tasks/change_password.rb index a038f5ec5..0a16acbfe 100644 --- a/app/tasks/change_password.rb +++ b/app/tasks/change_password.rb @@ -3,7 +3,11 @@ require_relative "../commands/users/change_user_password" class ChangePassword - def initialize(command = ChangeUserPassword.new, output: $stdout, input: $stdin) + def initialize( + command = ChangeUserPassword.new, + output: $stdout, + input: $stdin + ) @command = command @output = output @input = input diff --git a/config/puma.rb b/config/puma.rb index 24fa3537c..dd0037c77 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -26,7 +26,8 @@ on_worker_boot do if defined?(ActiveRecord::Base) env = ENV["RACK_ENV"] || "development" - config = YAML.safe_load(ERB.new(File.read("config/database.yml")).result)[env] + config = + YAML.safe_load(ERB.new(File.read("config/database.yml")).result)[env] ActiveRecord::Base.establish_connection(config) end end diff --git a/spec/commands/users/create_user_spec.rb b/spec/commands/users/create_user_spec.rb index ffbcc5133..189f44900 100644 --- a/spec/commands/users/create_user_spec.rb +++ b/spec/commands/users/create_user_spec.rb @@ -6,7 +6,7 @@ let(:repo) { double } describe "#create" do - it "remove any existing users and create a user with the password supplied" do + it "removes existing users and create a user with the password supplied" do command = CreateUser.new(repo) expect(repo).to receive(:create) diff --git a/spec/fever_api/read_groups_spec.rb b/spec/fever_api/read_groups_spec.rb index 6c5bbf33d..6adc080ff 100644 --- a/spec/fever_api/read_groups_spec.rb +++ b/spec/fever_api/read_groups_spec.rb @@ -4,7 +4,9 @@ describe FeverAPI::ReadGroups do let(:group1) { double("group1", as_fever_json: { id: 1, title: "IT news" }) } - let(:group2) { double("group2", as_fever_json: { id: 2, title: "World news" }) } + let(:group2) do + double("group2", as_fever_json: { id: 2, title: "World news" }) + end let(:group_repository) { double("repo") } subject { FeverAPI::ReadGroups.new(group_repository: group_repository) } diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index b2d0c7d15..0babe6f70 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -16,7 +16,8 @@ title: "", content: "" ).as_null_object - expect(StoryRepository).to receive(:normalize_url).with(entry.url, feed.url) + expect(StoryRepository) + .to receive(:normalize_url).with(entry.url, feed.url) StoryRepository.add(entry, feed) end @@ -461,7 +462,8 @@ end it "falls back to summary if there is no content" do - expect(StoryRepository.extract_content(summary_only)).to eq "Dumb publisher" + expect(StoryRepository.extract_content(summary_only)) + .to eq "Dumb publisher" end it "returns empty string if there is no content or summary" do @@ -477,7 +479,8 @@ summary: "Page" ) - expect(StoryRepository.extract_content(entry)).to eq "Page" + expect(StoryRepository.extract_content(entry)) + .to eq "Page" end it "ignores URL expansion if entry url is nil" do diff --git a/spec/tasks/remove_old_stories_spec.rb b/spec/tasks/remove_old_stories_spec.rb index cca650096..2bba7bc47 100644 --- a/spec/tasks/remove_old_stories_spec.rb +++ b/spec/tasks/remove_old_stories_spec.rb @@ -35,7 +35,8 @@ stories end - expect(FeedRepository).to receive(:fetch_by_ids).with([3, 5]).and_return([]) + expect(FeedRepository) + .to receive(:fetch_by_ids).with([3, 5]).and_return([]) RemoveOldStories.remove!(13) end From 83ea265bc4e5710823ecc7e19cf5d7255bdc37b8 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 18 Dec 2022 23:11:37 -0800 Subject: [PATCH 0420/1107] RuboCop: reduce line length to 80 (#724) --- .rubocop.yml | 2 +- app/fever_api/write_mark_item.rb | 5 ++++- app/models/migration_status.rb | 4 ++-- config/asset_pipeline.rb | 14 ++++++++++++-- spec/commands/feeds/import_from_opml_spec.rb | 7 ++++++- spec/controllers/feeds_controller_spec.rb | 6 +++--- spec/repositories/story_repository_spec.rb | 3 ++- spec/tasks/fetch_feed_spec.rb | 5 ++++- spec/utils/content_sanitizer_spec.rb | 7 ++++++- 9 files changed, 40 insertions(+), 13 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 03f17941d..4bba0aa0d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,7 +12,7 @@ AllCops: - 'vendor/**/*' Layout/LineLength: - Max: 81 + Max: 80 Metrics/BlockLength: Exclude: diff --git a/app/fever_api/write_mark_item.rb b/app/fever_api/write_mark_item.rb index 0ac82c1b4..fe18a78b4 100644 --- a/app/fever_api/write_mark_item.rb +++ b/app/fever_api/write_mark_item.rb @@ -7,7 +7,10 @@ module FeverAPI class WriteMarkItem def initialize(options = {}) @read_marker_class = options.fetch(:read_marker_class) { MarkAsRead } - @unread_marker_class = options.fetch(:unread_marker_class) { MarkAsUnread } + @unread_marker_class = + options.fetch(:unread_marker_class) do + MarkAsUnread + end @starred_marker_class = options.fetch(:starred_marker_class) { MarkAsStarred } @unstarred_marker_class = diff --git a/app/models/migration_status.rb b/app/models/migration_status.rb index 6c4d3dc41..a358ad49d 100644 --- a/app/models/migration_status.rb +++ b/app/models/migration_status.rb @@ -1,8 +1,8 @@ class MigrationStatus attr_reader :migrator - def initialize(migrator = ActiveRecord::Base.connection.migration_context.open) - @migrator = migrator + def initialize(migrator = nil) + @migrator = migrator || ActiveRecord::Base.connection.migration_context.open end def pending_migrations diff --git a/config/asset_pipeline.rb b/config/asset_pipeline.rb index 914008343..32f2b2a83 100644 --- a/config/asset_pipeline.rb +++ b/config/asset_pipeline.rb @@ -19,8 +19,18 @@ def registered(app) def append_paths(app) app.sprockets.append_path File.join(app.root, "app", "assets") - app.sprockets.append_path File.join(app.root, "app", "assets", "stylesheets") - app.sprockets.append_path File.join(app.root, "app", "assets", "javascripts") + app.sprockets.append_path File.join( + app.root, + "app", + "assets", + "stylesheets" + ) + app.sprockets.append_path File.join( + app.root, + "app", + "assets", + "javascripts" + ) end def configure_development(app) diff --git a/spec/commands/feeds/import_from_opml_spec.rb b/spec/commands/feeds/import_from_opml_spec.rb index be2e7a9e4..8a3386b1d 100644 --- a/spec/commands/feeds/import_from_opml_spec.rb +++ b/spec/commands/feeds/import_from_opml_spec.rb @@ -4,7 +4,12 @@ describe ImportFromOpml do let(:subscriptions) do - File.open(File.expand_path("../../support/files/subscriptions.xml", __dir__)) + File.open( + File.expand_path( + "../../support/files/subscriptions.xml", + __dir__ + ) + ) end def import diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index e070f07c3..170cb3373 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -100,11 +100,11 @@ def params(feed, **overrides) describe "POST /feeds" do context "when the feed url is valid" do let(:feed_url) { "http://example.com/" } - let(:valid_feed) { double(valid?: true) } + let(:feed) { double(valid?: true) } it "adds the feed and queues it to be fetched" do - expect(AddNewFeed).to receive(:add).with(feed_url).and_return(valid_feed) - expect(FetchFeeds).to receive(:enqueue).with([valid_feed]) + expect(AddNewFeed).to receive(:add).with(feed_url).and_return(feed) + expect(FetchFeeds).to receive(:enqueue).with([feed]) post("/feeds", feed_url:) diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 0babe6f70..d45af9afe 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -361,7 +361,8 @@ it "returns unstarred read stories older than given number of days" do story = create_story(:read, published: 6.days.ago) - expect(StoryRepository.unstarred_read_stories_older_than(5)).to eq([story]) + expect(StoryRepository.unstarred_read_stories_older_than(5)) + .to eq([story]) end it "does not return starred stories older than the given number of days" do diff --git a/spec/tasks/fetch_feed_spec.rb b/spec/tasks/fetch_feed_spec.rb index 6d99204f0..a5bc4d88b 100644 --- a/spec/tasks/fetch_feed_spec.rb +++ b/spec/tasks/fetch_feed_spec.rb @@ -82,7 +82,10 @@ end it "should only add posts that are new" do - expect(StoryRepository).to receive(:add).with(new_story, daring_fireball) + expect(StoryRepository).to receive(:add).with( + new_story, + daring_fireball + ) expect(StoryRepository) .not_to receive(:add).with(old_story, daring_fireball) diff --git a/spec/utils/content_sanitizer_spec.rb b/spec/utils/content_sanitizer_spec.rb index 9fd3e18a7..a5056fb12 100644 --- a/spec/utils/content_sanitizer_spec.rb +++ b/spec/utils/content_sanitizer_spec.rb @@ -6,22 +6,27 @@ describe ".sanitize" do context "regressions" do it "handles tag properly" do - result = described_class.sanitize("WM_ERROR asdf") + result = + described_class.sanitize("WM_ERROR asdf") + expect(result).to eq "WM_ERROR asdf" end it "handles
    tag properly" do result = described_class.sanitize("
    some code
    ") + expect(result).to eq "
    some code
    " end it "handles unprintable characters" do result = described_class.sanitize("n\u2028\u2029") + expect(result).to eq "n" end it "preserves line endings" do result = described_class.sanitize("test\r\ncase") + expect(result).to eq "test\r\ncase" end end From da95293c5fa3c24185c1bd1206cac60947640ddf Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Mon, 19 Dec 2022 08:50:45 -0800 Subject: [PATCH 0421/1107] RuboCop: clean up .rubocop.yml (#725) --- .rubocop.yml | 44 ++++-------------- .rubocop_todo.yml | 114 ++++++++++++++++++++-------------------------- 2 files changed, 60 insertions(+), 98 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 4bba0aa0d..3fa9fe2e2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -11,45 +11,18 @@ AllCops: - 'db/schema.rb' - 'vendor/**/*' -Layout/LineLength: - Max: 80 - -Metrics/BlockLength: - Exclude: - - 'spec/**/*_spec.rb' - -Metrics/MethodLength: - Max: 15 - -Style/ConstantVisibility: - Enabled: false - -Style/Copyright: - Enabled: false - -Style/Documentation: - Enabled: false - -Style/DocumentationMethod: - Enabled: false - -Style/DoubleNegation: - Enabled: false - -Style/MissingElse: - Enabled: false - -Style/NumericLiterals: - Enabled: false - -Style/StringLiterals: - EnforcedStyle: double_quotes - +Layout/LineLength: { Max: 80 } +Metrics/BlockLength: { Exclude: ['spec/**/*_spec.rb'] } Style/MethodCallWithArgsParentheses: AllowedMethods: - to - not_to - describe +Style/StringLiterals: { EnforcedStyle: double_quotes } + +# want to enable these, but they don't work right when using `.rubocop_todo.yml` +Style/DocumentationMethod: { Enabled: false } +Style/Documentation: { Enabled: false } ################################################################################ # @@ -63,3 +36,6 @@ RSpec/StubbedMock: { Enabled: false } Style/InlineComment: { Enabled: false } Style/RequireOrder: { Enabled: false } Style/SafeNavigation: { Enabled: false } +Style/ConstantVisibility: { Enabled: false } +Style/Copyright: { Enabled: false } +Style/MissingElse: { Enabled: false } diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index daafc95d8..921c1bbb3 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 400` -# on 2022-12-19 02:34:30 UTC using RuboCop version 1.40.0. +# on 2022-12-19 06:51:16 UTC using RuboCop version 1.40.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -21,21 +21,13 @@ Bundler/GemVersion: Exclude: - 'Gemfile' -# Offense count: 25 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: InspectBlocks. Layout/RedundantLineBreak: Exclude: - - 'app/commands/feeds/export_to_opml.rb' - - 'app/repositories/story_repository.rb' - - 'app/utils/content_sanitizer.rb' - 'spec/factories/user_factory.rb' - 'spec/factories/users.rb' - - 'spec/fever_api/read_feeds_spec.rb' - - 'spec/helpers/url_helpers_spec.rb' - - 'spec/integration/feed_importing_spec.rb' - - 'spec/repositories/story_repository_spec.rb' - - 'spec/tasks/fetch_feed_spec.rb' # Offense count: 8 # This cop supports safe autocorrection (--autocorrect). @@ -46,11 +38,10 @@ Layout/SingleLineBlockChain: - 'spec/models/story_spec.rb' - 'spec/tasks/fetch_feeds_spec.rb' -# Offense count: 2 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Lint/AmbiguousOperatorPrecedence: Exclude: - - 'spec/factories/feed_factory.rb' - 'spec/factories/group_factory.rb' # Offense count: 1 @@ -59,7 +50,7 @@ Lint/EmptyBlock: Exclude: - 'spec/repositories/story_repository_spec.rb' -# Offense count: 13 +# Offense count: 12 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, IgnoredClasses. # AllowedMethods: ago, from_now, second, seconds, minute, minutes, hour, hours, day, days, week, weeks, fortnight, fortnights, in_milliseconds @@ -85,6 +76,22 @@ Metrics/AbcSize: Exclude: - 'app/controllers/feeds_controller.rb' +# Offense count: 14 +# Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods. +Metrics/MethodLength: + Exclude: + - 'app/controllers/feeds_controller.rb' + - 'app/fever_api/read_items.rb' + - 'app/helpers/url_helpers.rb' + - 'app/models/story.rb' + - 'app/repositories/story_repository.rb' + - 'app/tasks/fetch_feeds.rb' + - 'app/utils/opml_parser.rb' + - 'app/utils/sample_story.rb' + - 'config/asset_pipeline.rb' + - 'db/migrate/20130425222157_add_delayed_job.rb' + - 'spec/factories/story_factory.rb' + # Offense count: 9 # Configuration parameters: ForbiddenDelimiters. # ForbiddenDelimiters: (?-mix:(^|\s)(EO[A-Z]{1}|END)(\s|$)) @@ -94,7 +101,7 @@ Naming/HeredocDelimiterNaming: - 'spec/helpers/url_helpers_spec.rb' - 'spec/utils/opml_parser_spec.rb' -# Offense count: 35 +# Offense count: 31 # This cop supports safe autocorrection (--autocorrect). RSpec/AlignLeftLetBrace: Exclude: @@ -108,8 +115,6 @@ RSpec/AlignLeftLetBrace: - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api/read_feeds_groups_spec.rb' - - 'spec/fever_api/read_feeds_spec.rb' - - 'spec/fever_api/read_groups_spec.rb' - 'spec/fever_api/sync_saved_item_ids_spec.rb' - 'spec/fever_api/sync_unread_item_ids_spec.rb' - 'spec/fever_api/write_mark_feed_spec.rb' @@ -119,7 +124,7 @@ RSpec/AlignLeftLetBrace: - 'spec/tasks/fetch_feeds_spec.rb' - 'spec/utils/feed_discovery_spec.rb' -# Offense count: 40 +# Offense count: 35 # This cop supports safe autocorrection (--autocorrect). RSpec/AlignRightLetBrace: Exclude: @@ -133,8 +138,6 @@ RSpec/AlignRightLetBrace: - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api/read_feeds_groups_spec.rb' - - 'spec/fever_api/read_feeds_spec.rb' - - 'spec/fever_api/read_groups_spec.rb' - 'spec/fever_api/sync_saved_item_ids_spec.rb' - 'spec/fever_api/sync_unread_item_ids_spec.rb' - 'spec/fever_api/write_mark_feed_spec.rb' @@ -144,7 +147,7 @@ RSpec/AlignRightLetBrace: - 'spec/tasks/fetch_feeds_spec.rb' - 'spec/utils/feed_discovery_spec.rb' -# Offense count: 6 +# Offense count: 5 RSpec/AnyInstance: Exclude: - 'spec/controllers/exports_controller_spec.rb' @@ -184,7 +187,7 @@ RSpec/DescribeClass: - 'spec/integration/feed_importing_spec.rb' - 'spec/utils/i18n_support_spec.rb' -# Offense count: 145 +# Offense count: 149 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SkipBlocks, EnforcedStyle. # SupportedStyles: described_class, explicit @@ -227,12 +230,11 @@ RSpec/DescribedClass: - 'spec/utils/feed_discovery_spec.rb' - 'spec/utils/opml_parser_spec.rb' -# Offense count: 9 +# Offense count: 7 # This cop supports safe autocorrection (--autocorrect). RSpec/EmptyLineAfterFinalLet: Exclude: - 'spec/commands/feeds/add_new_feed_spec.rb' - - 'spec/commands/feeds/import_from_opml_spec.rb' - 'spec/commands/users/complete_setup_spec.rb' - 'spec/controllers/exports_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' @@ -246,7 +248,7 @@ RSpec/EmptyLineAfterHook: Exclude: - 'spec/controllers/stories_controller_spec.rb' -# Offense count: 56 +# Offense count: 63 # Configuration parameters: Max, CountAsOne. RSpec/ExampleLength: Exclude: @@ -343,7 +345,7 @@ RSpec/MessageChain: - 'spec/fever_api/read_feeds_groups_spec.rb' - 'spec/fever_api_spec.rb' -# Offense count: 102 +# Offense count: 106 # Configuration parameters: EnforcedStyle. # SupportedStyles: allow, expect RSpec/MessageExpectation: @@ -378,7 +380,7 @@ RSpec/MessageExpectation: - 'spec/tasks/remove_old_stories_spec.rb' - 'spec/utils/feed_discovery_spec.rb' -# Offense count: 107 +# Offense count: 111 # Configuration parameters: EnforcedStyle. # SupportedStyles: have_received, receive RSpec/MessageSpies: @@ -413,7 +415,7 @@ RSpec/MessageSpies: - 'spec/tasks/remove_old_stories_spec.rb' - 'spec/utils/feed_discovery_spec.rb' -# Offense count: 97 +# Offense count: 96 # Configuration parameters: Max. RSpec/MultipleExpectations: Exclude: @@ -426,7 +428,6 @@ RSpec/MultipleExpectations: - 'spec/commands/users/complete_setup_spec.rb' - 'spec/commands/users/create_user_spec.rb' - 'spec/controllers/debug_controller_spec.rb' - - 'spec/controllers/exports_controller_spec.rb' - 'spec/controllers/feeds_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/imports_controller_spec.rb' @@ -515,32 +516,7 @@ RSpec/ScatteredLet: - 'spec/commands/feeds/import_from_opml_spec.rb' - 'spec/repositories/feed_repository_spec.rb' -# Offense count: 55 -RSpec/StubbedMock: - Exclude: - - 'spec/commands/feeds/add_new_feed_spec.rb' - - 'spec/commands/stories/mark_group_as_read_spec.rb' - - 'spec/commands/users/change_user_password_spec.rb' - - 'spec/controllers/exports_controller_spec.rb' - - 'spec/controllers/feeds_controller_spec.rb' - - 'spec/controllers/first_run_controller_spec.rb' - - 'spec/controllers/stories_controller_spec.rb' - - 'spec/fever_api/authentication_spec.rb' - - 'spec/fever_api/read_feeds_spec.rb' - - 'spec/fever_api/read_groups_spec.rb' - - 'spec/fever_api/read_items_spec.rb' - - 'spec/fever_api/sync_saved_item_ids_spec.rb' - - 'spec/fever_api/sync_unread_item_ids_spec.rb' - - 'spec/fever_api/write_mark_feed_spec.rb' - - 'spec/fever_api/write_mark_group_spec.rb' - - 'spec/fever_api/write_mark_item_spec.rb' - - 'spec/fever_api_spec.rb' - - 'spec/jobs/fetch_feed_job_spec.rb' - - 'spec/repositories/feed_repository_spec.rb' - - 'spec/tasks/remove_old_stories_spec.rb' - - 'spec/utils/feed_discovery_spec.rb' - -# Offense count: 93 +# Offense count: 94 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Exclude: @@ -579,7 +555,7 @@ Rails/BulkChangeTable: - 'db/migrate/20130412185253_add_new_fields_to_stories.rb' - 'db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb' -# Offense count: 2 +# Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Whitelist, AllowedMethods, AllowedReceivers. # Whitelist: find_by_sql, find_by_token_for @@ -596,7 +572,7 @@ Rails/HasManyOrHasOneDependent: Exclude: - 'app/models/group.rb' -# Offense count: 27 +# Offense count: 26 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Include. # Include: spec/**/*, test/**/* @@ -737,6 +713,14 @@ Style/DisableCopsWithinSourceCodeDirective: - 'spec/helpers/authentications_helper_spec.rb' - 'spec/helpers/url_helpers_spec.rb' +# Offense count: 3 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: allowed_in_returns, forbidden +Style/DoubleNegation: + Exclude: + - 'app/controllers/sinatra/stories_controller.rb' + # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedVars. @@ -904,7 +888,7 @@ Style/FrozenStringLiteralComment: - 'spec/utils/i18n_support_spec.rb' - 'spec/utils/opml_parser_spec.rb' -# Offense count: 86 +# Offense count: 91 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys @@ -945,7 +929,7 @@ Style/HashSyntax: - 'spec/tasks/change_password_spec.rb' - 'spec/tasks/fetch_feed_spec.rb' -# Offense count: 184 +# Offense count: 185 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: IgnoreMacros, AllowedMethods, IgnoredMethods, AllowedPatterns, IgnoredPatterns, IncludedMacros, AllowParenthesesInMultilineCall, AllowParenthesesInChaining, AllowParenthesesInCamelCaseMethod, AllowParenthesesInStringInterpolation, EnforcedStyle. # SupportedStyles: require_parentheses, omit_parentheses @@ -1004,13 +988,15 @@ Style/MethodCallWithArgsParentheses: - 'spec/utils/i18n_support_spec.rb' - 'spec/utils/opml_parser_spec.rb' -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns, IgnoredMethods. -# SupportedStyles: predicate, comparison -Style/NumericPredicate: +# Offense count: 10 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: MinDigits, Strict, AllowedNumbers, AllowedPatterns. +Style/NumericLiterals: Exclude: - - 'app/commands/stories/mark_group_as_read.rb' + - 'spec/fever_api/authentication_spec.rb' + - 'spec/fever_api/write_mark_feed_spec.rb' + - 'spec/fever_api/write_mark_group_spec.rb' + - 'spec/fever_api_spec.rb' # Offense count: 6 Style/OpenStructUse: From 7fafa8cba5203c0efe2f4e3e152f9389cbdc3103 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Mon, 19 Dec 2022 08:50:59 -0800 Subject: [PATCH 0422/1107] Update all Bundler dependencies (2022-12-19) (#718) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- Gemfile.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e96b88b78..c2881390d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -89,7 +89,7 @@ GEM thor (>= 0.20.3, < 2.0) tins (~> 1.16) crass (1.0.6) - date (3.3.2) + date (3.3.3) delayed_job (4.1.11) activesupport (>= 3.0, < 8.0) delayed_job_active_record (4.1.7) @@ -164,7 +164,7 @@ GEM nio4r (~> 2.0) racc (1.6.1) rack (2.2.4) - rack-protection (3.0.4) + rack-protection (3.0.5) rack rack-ssl (1.4.1) rack @@ -212,7 +212,7 @@ GEM rspec-mocks (~> 3.12.0) rspec-core (3.12.0) rspec-support (~> 3.12.0) - rspec-expectations (3.12.0) + rspec-expectations (3.12.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-html-matchers (0.10.0) @@ -248,7 +248,7 @@ GEM rubocop (>= 1.33.0, < 2.0) rubocop-rake (0.6.0) rubocop (~> 1.0) - rubocop-rspec (2.15.0) + rubocop-rspec (2.16.0) rubocop (~> 1.33) ruby-progressbar (1.11.0) ruby2_keywords (0.0.5) @@ -266,19 +266,19 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - sinatra (3.0.4) + sinatra (3.0.5) mustermann (~> 3.0) rack (~> 2.2, >= 2.2.4) - rack-protection (= 3.0.4) + rack-protection (= 3.0.5) tilt (~> 2.0) sinatra-activerecord (2.0.26) activerecord (>= 4.1) sinatra (>= 1.0) - sinatra-contrib (3.0.4) + sinatra-contrib (3.0.5) multi_json mustermann (~> 3.0) - rack-protection (= 3.0.4) - sinatra (= 3.0.4) + rack-protection (= 3.0.5) + sinatra (= 3.0.5) tilt (~> 2.0) sinatra-flash (0.3.0) sinatra (>= 1.0.0) From 73ca19fbf10a0b27e03f78cdf49397f1d11d015c Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 20 Dec 2022 11:26:12 -0800 Subject: [PATCH 0423/1107] RuboCop: move gem lints to not wanted (#726) --- .rubocop.yml | 10 ++++++---- .rubocop_todo.yml | 15 --------------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 3fa9fe2e2..40ad0144f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -30,12 +30,14 @@ Style/Documentation: { Enabled: false } # ################################################################################ +Bundler/GemComment: { Enabled: false } +Bundler/GemVersion: { Enabled: false } Lint/ConstantResolution: { Enabled: false } -Rails/SchemaComment: { Enabled: false } RSpec/StubbedMock: { Enabled: false } -Style/InlineComment: { Enabled: false } -Style/RequireOrder: { Enabled: false } -Style/SafeNavigation: { Enabled: false } +Rails/SchemaComment: { Enabled: false } Style/ConstantVisibility: { Enabled: false } Style/Copyright: { Enabled: false } +Style/InlineComment: { Enabled: false } Style/MissingElse: { Enabled: false } +Style/RequireOrder: { Enabled: false } +Style/SafeNavigation: { Enabled: false } diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 921c1bbb3..1217ea77b 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,21 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 35 -# Configuration parameters: Include, IgnoredGems, OnlyFor. -# Include: **/*.gemfile, **/Gemfile, **/gems.rb -Bundler/GemComment: - Exclude: - - 'Gemfile' - -# Offense count: 33 -# Configuration parameters: EnforcedStyle, Include, AllowedGems. -# SupportedStyles: required, forbidden -# Include: **/*.gemfile, **/Gemfile, **/gems.rb -Bundler/GemVersion: - Exclude: - - 'Gemfile' - # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: InspectBlocks. From 291885f180a66ebc83c5eff3c677b82eb5d1822f Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 20 Dec 2022 11:38:27 -0800 Subject: [PATCH 0424/1107] RuboCop: fix RedundantLineBreak offenses (#727) --- .rubocop_todo.yml | 8 -------- spec/factories/user_factory.rb | 5 +---- spec/factories/users.rb | 4 +--- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 1217ea77b..b97d82108 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,14 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: InspectBlocks. -Layout/RedundantLineBreak: - Exclude: - - 'spec/factories/user_factory.rb' - - 'spec/factories/users.rb' - # Offense count: 8 # This cop supports safe autocorrection (--autocorrect). Layout/SingleLineBlockChain: diff --git a/spec/factories/user_factory.rb b/spec/factories/user_factory.rb index ced5ad345..108370ee4 100644 --- a/spec/factories/user_factory.rb +++ b/spec/factories/user_factory.rb @@ -4,9 +4,6 @@ class UserFactory class FakeUser < OpenStruct; end def self.build - FakeUser.new( - id: rand(100), - setup_complete: false - ) + FakeUser.new(id: rand(100), setup_complete: false) end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 4a76ebe07..84d6f494e 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,7 +1,5 @@ module Factories - USER_TRAITS = { - setup_complete: -> { { setup_complete: true } } - }.freeze + USER_TRAITS = { setup_complete: -> { { setup_complete: true } } }.freeze def create_user(*traits, **params) build_user(*traits, **params).tap(&:save!) From dcb4e86aae7df31fd6e7791324ea523b09c1a1c8 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 20 Dec 2022 11:49:34 -0800 Subject: [PATCH 0425/1107] RuboCop: move SingleLineBlockChain to not wanted (#728) --- .rubocop.yml | 1 + .rubocop_todo.yml | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 40ad0144f..824da5e97 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -32,6 +32,7 @@ Style/Documentation: { Enabled: false } Bundler/GemComment: { Enabled: false } Bundler/GemVersion: { Enabled: false } +Layout/SingleLineBlockChain: { Enabled: false } Lint/ConstantResolution: { Enabled: false } RSpec/StubbedMock: { Enabled: false } Rails/SchemaComment: { Enabled: false } diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b97d82108..f657c76f7 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,15 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 8 -# This cop supports safe autocorrection (--autocorrect). -Layout/SingleLineBlockChain: - Exclude: - - 'app/tasks/change_password.rb' - - 'spec/integration/feed_importing_spec.rb' - - 'spec/models/story_spec.rb' - - 'spec/tasks/fetch_feeds_spec.rb' - # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Lint/AmbiguousOperatorPrecedence: From b0ea0947ff4c44579f72b34b8dc607112f677920 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 20 Dec 2022 12:16:17 -0800 Subject: [PATCH 0426/1107] RuboCop: fix AmbiguousOperatorPrecedence offense (#729) --- .rubocop_todo.yml | 6 ------ spec/factories/group_factory.rb | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f657c76f7..fdc62e1a4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,12 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Lint/AmbiguousOperatorPrecedence: - Exclude: - - 'spec/factories/group_factory.rb' - # Offense count: 1 # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: diff --git a/spec/factories/group_factory.rb b/spec/factories/group_factory.rb index bcbcd6d00..58814dab5 100644 --- a/spec/factories/group_factory.rb +++ b/spec/factories/group_factory.rb @@ -11,7 +11,7 @@ def as_fever_json def self.build(params = {}) FakeGroup.new( id: rand(100), - name: params[:name] || Faker::Name.name + " group" # rubocop:disable Style/StringConcatenation + name: params[:name] || "#{Faker::Name.name} group" ) end end From 645e24d8cd918b951b69b27d0cfd30f95230f446 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 20 Dec 2022 12:56:51 -0800 Subject: [PATCH 0427/1107] RuboCop: remove in-line disables (#730) --- .rubocop_todo.yml | 43 ++++++++++++++------- app/commands/feeds/export_to_opml.rb | 2 +- app/utils/sample_story.rb | 6 +-- spec/factories/feed_factory.rb | 2 +- spec/helpers/authentications_helper_spec.rb | 2 +- spec/helpers/url_helpers_spec.rb | 2 +- 6 files changed, 36 insertions(+), 21 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index fdc62e1a4..e3d3ead66 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,11 +1,20 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 400` -# on 2022-12-19 06:51:16 UTC using RuboCop version 1.40.0. +# on 2022-12-19 07:00:21 UTC using RuboCop version 1.40.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. +# Offense count: 3 +# Configuration parameters: AllowedMethods. +# AllowedMethods: enums +Lint/ConstantDefinitionInBlock: + Exclude: + - 'app/utils/sample_story.rb' + - 'spec/helpers/authentications_helper_spec.rb' + - 'spec/helpers/url_helpers_spec.rb' + # Offense count: 1 # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: @@ -38,10 +47,11 @@ Metrics/AbcSize: Exclude: - 'app/controllers/feeds_controller.rb' -# Offense count: 14 +# Offense count: 15 # Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods. Metrics/MethodLength: Exclude: + - 'app/commands/feeds/export_to_opml.rb' - 'app/controllers/feeds_controller.rb' - 'app/fever_api/read_items.rb' - 'app/helpers/url_helpers.rb' @@ -63,6 +73,16 @@ Naming/HeredocDelimiterNaming: - 'spec/helpers/url_helpers_spec.rb' - 'spec/utils/opml_parser_spec.rb' +# Offense count: 2 +# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros. +# NamePrefix: is_, has_, have_ +# ForbiddenPrefixes: is_, has_, have_ +# AllowedMethods: is_a? +# MethodDefinitionMacros: define_method, define_singleton_method +Naming/PredicateName: + Exclude: + - 'app/utils/sample_story.rb' + # Offense count: 31 # This cop supports safe autocorrection (--autocorrect). RSpec/AlignLeftLetBrace: @@ -663,18 +683,6 @@ Style/CollectionMethods: - 'app/controllers/sinatra/stories_controller.rb' - 'app/fever_api/response.rb' -# Offense count: 8 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedCops. -Style/DisableCopsWithinSourceCodeDirective: - Exclude: - - 'app/commands/feeds/export_to_opml.rb' - - 'app/utils/sample_story.rb' - - 'spec/factories/feed_factory.rb' - - 'spec/factories/group_factory.rb' - - 'spec/helpers/authentications_helper_spec.rb' - - 'spec/helpers/url_helpers_spec.rb' - # Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. @@ -1036,6 +1044,13 @@ Style/StaticClass: - 'app/utils/api_key.rb' - 'app/utils/content_sanitizer.rb' +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Mode. +Style/StringConcatenation: + Exclude: + - 'spec/factories/feed_factory.rb' + # Offense count: 19 # This cop supports unsafe autocorrection (--autocorrect-all). Style/StringHashKeys: diff --git a/app/commands/feeds/export_to_opml.rb b/app/commands/feeds/export_to_opml.rb index 38715904e..c6e37810c 100644 --- a/app/commands/feeds/export_to_opml.rb +++ b/app/commands/feeds/export_to_opml.rb @@ -5,7 +5,7 @@ def initialize(feeds) @feeds = feeds end - def to_xml # rubocop:disable Metrics/MethodLength + def to_xml builder = Nokogiri::XML::Builder.new do |xml| xml.opml(version: "1.0") do diff --git a/app/utils/sample_story.rb b/app/utils/sample_story.rb index 7c3985e40..efb6b9edc 100644 --- a/app/utils/sample_story.rb +++ b/app/utils/sample_story.rb @@ -1,6 +1,6 @@ SampleStory = Struct.new(:source, :title, :lead, :is_read, :published) do - BODY = <<~EOS.freeze # rubocop:disable Lint/ConstantDefinitionInBlock + BODY = <<~EOS.freeze

    Tofu shoreditch intelligentsia umami, fashion axe photo booth try-hard terry richardson quinoa actually fingerstache meggings fixie. Aesthetic salvia vinyl raw denim, keffiyeh master cleanse tonx selfies mlkshk occupy twee @@ -41,7 +41,7 @@ def body BODY end - def is_read # rubocop:disable Naming/PredicateName + def is_read false end @@ -49,7 +49,7 @@ def keep_unread false end - def is_starred # rubocop:disable Naming/PredicateName + def is_starred false end diff --git a/spec/factories/feed_factory.rb b/spec/factories/feed_factory.rb index 3355d2374..b649894a6 100644 --- a/spec/factories/feed_factory.rb +++ b/spec/factories/feed_factory.rb @@ -17,7 +17,7 @@ def self.build(params = {}) FakeFeed.new( id: rand(100), group_id: rand(100), - name: Faker::Name.name + " on Software", # rubocop:disable Style/StringConcatenation + name: Faker::Name.name + " on Software", url: Faker::Internet.url, last_fetched: Time.now, stories: [], diff --git a/spec/helpers/authentications_helper_spec.rb b/spec/helpers/authentications_helper_spec.rb index 0db0086a1..b7457b884 100644 --- a/spec/helpers/authentications_helper_spec.rb +++ b/spec/helpers/authentications_helper_spec.rb @@ -3,7 +3,7 @@ app_require "helpers/authentication_helpers" RSpec.describe Sinatra::AuthenticationHelpers do - class Helper # rubocop:disable Lint/ConstantDefinitionInBlock + class Helper include Sinatra::AuthenticationHelpers end diff --git a/spec/helpers/url_helpers_spec.rb b/spec/helpers/url_helpers_spec.rb index e49fc7ebe..31a935aa8 100644 --- a/spec/helpers/url_helpers_spec.rb +++ b/spec/helpers/url_helpers_spec.rb @@ -3,7 +3,7 @@ app_require "helpers/url_helpers" RSpec.describe UrlHelpers do - class Helper # rubocop:disable Lint/ConstantDefinitionInBlock + class Helper include UrlHelpers end From 9f3bcbf06de26c2f1316b7e586e4127e61350fbd Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 20 Dec 2022 13:25:07 -0800 Subject: [PATCH 0428/1107] RuboCop: enable ConstantDefinitionInBlock cop (#731) --- .rubocop_todo.yml | 9 ----- app/utils/sample_story.rb | 44 ++++++++++----------- spec/helpers/authentications_helper_spec.rb | 7 ++-- spec/helpers/url_helpers_spec.rb | 7 ++-- 4 files changed, 28 insertions(+), 39 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e3d3ead66..b2c36dd04 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,15 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 3 -# Configuration parameters: AllowedMethods. -# AllowedMethods: enums -Lint/ConstantDefinitionInBlock: - Exclude: - - 'app/utils/sample_story.rb' - - 'spec/helpers/authentications_helper_spec.rb' - - 'spec/helpers/url_helpers_spec.rb' - # Offense count: 1 # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: diff --git a/app/utils/sample_story.rb b/app/utils/sample_story.rb index efb6b9edc..469465990 100644 --- a/app/utils/sample_story.rb +++ b/app/utils/sample_story.rb @@ -1,26 +1,26 @@ +SAMPLE_BODY = <<~EOS.freeze +

    Tofu shoreditch intelligentsia umami, fashion axe photo booth + try-hard terry richardson quinoa actually fingerstache meggings fixie. Aesthetic + salvia vinyl raw denim, keffiyeh master cleanse tonx selfies mlkshk occupy twee + street art gentrify. Quinoa PBR readymade 90's. Chambray Austin aesthetic + meggings, carles vinyl intelligentsia tattooed. Keffiyeh mumblecore + fingerstache, sartorial sriracha disrupt biodiesel cred. Skateboard yr cosby + sweater, narwhal beard ethnic jean shorts aesthetic. Post-ironic flannel mlkshk, + pickled VHS wolf banjo forage portland wayfarers.

    + +

    Selfies mumblecore odd future irony DIY messenger bag. + Authentic neutra next level selvage squid. Four loko freegan occupy, tousled + vinyl leggings selvage messenger bag. Four loko wayfarers kale chips, next level + banksy banh mi umami flannel hella. Street art odd future scenester, + intelligentsia brunch fingerstache YOLO narwhal single-origin coffee tousled + tumblr pop-up four loko you probably haven't heard of them dreamcatcher. + Single-origin coffee direct trade retro biodiesel, truffaut fanny pack portland + blue bottle scenester bushwick. Skateboard squid fanny pack bushwick, photo + booth vice literally.

    +EOS + SampleStory = Struct.new(:source, :title, :lead, :is_read, :published) do - BODY = <<~EOS.freeze -

    Tofu shoreditch intelligentsia umami, fashion axe photo booth - try-hard terry richardson quinoa actually fingerstache meggings fixie. Aesthetic - salvia vinyl raw denim, keffiyeh master cleanse tonx selfies mlkshk occupy twee - street art gentrify. Quinoa PBR readymade 90's. Chambray Austin aesthetic - meggings, carles vinyl intelligentsia tattooed. Keffiyeh mumblecore - fingerstache, sartorial sriracha disrupt biodiesel cred. Skateboard yr cosby - sweater, narwhal beard ethnic jean shorts aesthetic. Post-ironic flannel mlkshk, - pickled VHS wolf banjo forage portland wayfarers.

    - -

    Selfies mumblecore odd future irony DIY messenger bag. - Authentic neutra next level selvage squid. Four loko freegan occupy, tousled - vinyl leggings selvage messenger bag. Four loko wayfarers kale chips, next level - banksy banh mi umami flannel hella. Street art odd future scenester, - intelligentsia brunch fingerstache YOLO narwhal single-origin coffee tousled - tumblr pop-up four loko you probably haven't heard of them dreamcatcher. - Single-origin coffee direct trade retro biodiesel, truffaut fanny pack portland - blue bottle scenester bushwick. Skateboard squid fanny pack bushwick, photo - booth vice literally.

    - EOS - def id -1 * rand(100) end @@ -38,7 +38,7 @@ def lead end def body - BODY + SAMPLE_BODY end def is_read diff --git a/spec/helpers/authentications_helper_spec.rb b/spec/helpers/authentications_helper_spec.rb index b7457b884..e66496e00 100644 --- a/spec/helpers/authentications_helper_spec.rb +++ b/spec/helpers/authentications_helper_spec.rb @@ -3,12 +3,11 @@ app_require "helpers/authentication_helpers" RSpec.describe Sinatra::AuthenticationHelpers do - class Helper - include Sinatra::AuthenticationHelpers + let(:helper) do + helper_class = Class.new { include Sinatra::AuthenticationHelpers } + helper_class.new end - let(:helper) { Helper.new } - describe "#needs_authentication?" do let(:authenticated_path) { "/news" } diff --git a/spec/helpers/url_helpers_spec.rb b/spec/helpers/url_helpers_spec.rb index 31a935aa8..7a422d2ee 100644 --- a/spec/helpers/url_helpers_spec.rb +++ b/spec/helpers/url_helpers_spec.rb @@ -3,12 +3,11 @@ app_require "helpers/url_helpers" RSpec.describe UrlHelpers do - class Helper - include UrlHelpers + let(:helper) do + helper_class = Class.new { include UrlHelpers } + helper_class.new end - let(:helper) { Helper.new } - describe "#expand_absolute_urls" do it "preserves existing absolute urls" do content = 'bar' From 487ab19bfb0fbebc4e1e7a9273c0124ebb317842 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 20 Dec 2022 15:35:17 -0800 Subject: [PATCH 0429/1107] RuboCop: fix EmptyBlock offense (#732) --- .rubocop_todo.yml | 6 ------ spec/repositories/story_repository_spec.rb | 3 --- 2 files changed, 9 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b2c36dd04..d8e2537a8 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,12 +6,6 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 1 -# Configuration parameters: AllowComments, AllowEmptyLambdas. -Lint/EmptyBlock: - Exclude: - - 'spec/repositories/story_repository_spec.rb' - # Offense count: 12 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, IgnoredClasses. diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index d45af9afe..623c3eb37 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -426,9 +426,6 @@ end describe ".extract_title" do - let(:entry) do - end - it "returns the title if there is a title" do entry = double(title: "title", summary: "summary") From c726f7a6a4f4447572c3840d84a74aebb43df2d0 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 20 Dec 2022 15:39:42 -0800 Subject: [PATCH 0430/1107] Factories: introduce FactoryBot (#733) --- .rubocop_todo.yml | 2 -- Gemfile | 1 + Gemfile.lock | 6 ++++ spec/app_spec.rb | 8 ++--- spec/commands/users/complete_setup_spec.rb | 2 +- spec/controllers/first_run_controller_spec.rb | 2 +- spec/factories.rb | 1 - spec/factories/user_factory.rb | 9 ------ spec/factories/users.rb | 16 ++++------ spec/repositories/user_repository_spec.rb | 12 +++---- spec/spec_helper.rb | 1 + spec/support/factory_bot.rb | 32 +++++++++++++++++++ 12 files changed, 58 insertions(+), 34 deletions(-) delete mode 100644 spec/factories/user_factory.rb create mode 100644 spec/support/factory_bot.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d8e2537a8..113d91945 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -803,7 +803,6 @@ Style/FrozenStringLiteralComment: - 'spec/factories/groups.rb' - 'spec/factories/stories.rb' - 'spec/factories/story_factory.rb' - - 'spec/factories/user_factory.rb' - 'spec/factories/users.rb' - 'spec/fever_api/authentication_spec.rb' - 'spec/fever_api/read_favicons_spec.rb' @@ -960,7 +959,6 @@ Style/OpenStructUse: - 'spec/factories/feed_factory.rb' - 'spec/factories/group_factory.rb' - 'spec/factories/story_factory.rb' - - 'spec/factories/user_factory.rb' # Offense count: 28 # Configuration parameters: SuspiciousParamNames, Allowlist. diff --git a/Gemfile b/Gemfile index 0574bd935..18c1bba94 100644 --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,7 @@ end group :development, :test do gem "capybara" gem "coveralls_reborn", require: false + gem "factory_bot_rails" gem "faker" gem "pry-byebug" gem "rspec" diff --git a/Gemfile.lock b/Gemfile.lock index c2881390d..a8c143966 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,6 +99,11 @@ GEM docile (1.4.0) erubi (1.11.0) execjs (2.8.1) + factory_bot (6.2.1) + activesupport (>= 5.0.0) + factory_bot_rails (6.2.0) + factory_bot (~> 6.2.0) + railties (>= 5.0.0) faker (3.0.0) i18n (>= 1.8.11, < 2) feedbag (1.0.0) @@ -319,6 +324,7 @@ DEPENDENCIES coveralls_reborn delayed_job delayed_job_active_record + factory_bot_rails faker feedbag feedjira diff --git a/spec/app_spec.rb b/spec/app_spec.rb index bdffb08d3..0d79af4cf 100644 --- a/spec/app_spec.rb +++ b/spec/app_spec.rb @@ -10,7 +10,7 @@ context "when user is not authenticated and page requires authentication" do it "sets the session redirect_to" do - create_user(:setup_complete) + create(:user, :setup_complete) get("/news") @@ -18,7 +18,7 @@ end it "redirects to /login" do - create_user(:setup_complete) + create(:user, :setup_complete) get("/news") @@ -28,7 +28,7 @@ end it "does not redirect when page needs no authentication" do - create_user(:setup_complete) + create(:user, :setup_complete) get("/login") @@ -36,7 +36,7 @@ end it "does not redirect when user is authenticated" do - user = create_user(:setup_complete) + user = create(:user, :setup_complete) get("/news", {}, "rack.session" => { user_id: user.id }) diff --git a/spec/commands/users/complete_setup_spec.rb b/spec/commands/users/complete_setup_spec.rb index b967d6929..cc938a7e1 100644 --- a/spec/commands/users/complete_setup_spec.rb +++ b/spec/commands/users/complete_setup_spec.rb @@ -3,7 +3,7 @@ app_require "commands/users/complete_setup" describe CompleteSetup do - let(:user) { UserFactory.build } + let(:user) { build(:user) } it "marks setup as complete" do expect(user).to receive(:save).once diff --git a/spec/controllers/first_run_controller_spec.rb b/spec/controllers/first_run_controller_spec.rb index 192a8e1b2..baf706c92 100644 --- a/spec/controllers/first_run_controller_spec.rb +++ b/spec/controllers/first_run_controller_spec.rb @@ -75,7 +75,7 @@ context "when a user has been setup" do it "should redirect any requests to first run stuff" do - user = create_user(:setup_complete) + user = create(:user, :setup_complete) session = { "rack.session" => { user_id: user.id } } get "/", {}, session diff --git a/spec/factories.rb b/spec/factories.rb index 4b0718ff2..6b2ba18e4 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,6 +1,5 @@ require_relative "factories/feed_factory" require_relative "factories/story_factory" -require_relative "factories/user_factory" require_relative "factories/group_factory" require_relative "factories/feeds" require_relative "factories/groups" diff --git a/spec/factories/user_factory.rb b/spec/factories/user_factory.rb deleted file mode 100644 index 108370ee4..000000000 --- a/spec/factories/user_factory.rb +++ /dev/null @@ -1,9 +0,0 @@ -require_relative "./feed_factory" - -class UserFactory - class FakeUser < OpenStruct; end - - def self.build - FakeUser.new(id: rand(100), setup_complete: false) - end -end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 84d6f494e..a3192ad95 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,13 +1,9 @@ -module Factories - USER_TRAITS = { setup_complete: -> { { setup_complete: true } } }.freeze +FactoryBot.define do + factory(:user) do + password { "super-secret" } - def create_user(*traits, **params) - build_user(*traits, **params).tap(&:save!) - end - - def build_user(*traits, **params) - traits.each { |trait| params.merge!(USER_TRAITS.fetch(trait).call) } - - User.new(password: "super-secret", **params) + trait :setup_complete do + setup_complete { true } + end end end diff --git a/spec/repositories/user_repository_spec.rb b/spec/repositories/user_repository_spec.rb index 26c051598..2e969b000 100644 --- a/spec/repositories/user_repository_spec.rb +++ b/spec/repositories/user_repository_spec.rb @@ -10,7 +10,7 @@ end it "returns the user for the given id" do - user = create_user + user = create(:user) expect(UserRepository.fetch(user.id)).to eq(user) end @@ -22,13 +22,13 @@ end it "returns false when user has not completed setup" do - create_user + create(:user) expect(UserRepository.setup_complete?).to be(false) end it "returns true when user has completed setup" do - create_user(setup_complete: true) + create(:user, :setup_complete) expect(UserRepository.setup_complete?).to be(true) end @@ -36,7 +36,7 @@ describe ".save" do it "saves the given user" do - user = build_user + user = build(:user) expect { UserRepository.save(user) } .to change(user, :persisted?).from(false).to(true) @@ -51,8 +51,8 @@ describe ".first" do it "returns the first user" do - user = create_user - create_user + user = create(:user) + create(:user) expect(UserRepository.first).to eq(user) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index db68aa468..6bfd9b0b5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -12,6 +12,7 @@ require "date" require_relative "support/coverage" +require_relative "support/factory_bot" require_relative "factories" require "./app" diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb new file mode 100644 index 000000000..84cf4a0c8 --- /dev/null +++ b/spec/support/factory_bot.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "factory_bot" + +module FactoryCache + def self.user + @user ||= FactoryBot.create(:user) + end + + def self.reset + @user = nil + end +end + +RSpec.configure do |config| + config.include(FactoryBot::Syntax::Methods) + + config.after do + FactoryBot.rewind_sequences + FactoryCache.reset + end +end + +module FactoryBot + module Syntax + module Methods + def default_user + FactoryCache.user + end + end + end +end From 50a854e26929984fe45fe20f3d6390ddcd705e26 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 20 Dec 2022 15:54:54 -0800 Subject: [PATCH 0431/1107] Factories: switch feeds to FactoryBot (#734) --- spec/factories/feeds.rb | 10 +++----- spec/factories/stories.rb | 2 +- spec/models/story_spec.rb | 4 ++-- spec/repositories/feed_repository_spec.rb | 25 +++++++++---------- spec/repositories/story_repository_spec.rb | 28 +++++++++++----------- spec/tasks/fetch_feeds_spec.rb | 4 ++-- 6 files changed, 33 insertions(+), 40 deletions(-) diff --git a/spec/factories/feeds.rb b/spec/factories/feeds.rb index e0a2f50f5..8fd25e7aa 100644 --- a/spec/factories/feeds.rb +++ b/spec/factories/feeds.rb @@ -1,9 +1,5 @@ -module Factories - def create_feed(params = {}) - build_feed(params).tap(&:save!) - end - - def build_feed(params = {}) - Feed.new(url: "https://exampoo.com/#{next_id}", **params) +FactoryBot.define do + factory(:feed) do + sequence(:url, 100) { |n| "http://exampoo.com/#{n}" } end end diff --git a/spec/factories/stories.rb b/spec/factories/stories.rb index c4c622868..4713b0521 100644 --- a/spec/factories/stories.rb +++ b/spec/factories/stories.rb @@ -12,6 +12,6 @@ def create_story(*traits, **params) def build_story(*traits, **params) traits.each { |trait| params.merge!(STORY_TRAITS.fetch(trait).call) } - Story.new(entry_id: next_id, feed: build_feed, **params) + Story.new(entry_id: next_id, feed: FactoryBot.build(:feed), **params) end end diff --git a/spec/models/story_spec.rb b/spec/models/story_spec.rb index 413e97558..ce43ea6cf 100644 --- a/spec/models/story_spec.rb +++ b/spec/models/story_spec.rb @@ -64,7 +64,7 @@ describe "#as_json" do it "returns a hash of the story" do - feed = create_feed(name: "my feed") + feed = create(:feed, name: "my feed") published_at = 1.day.ago created_at = 1.hour.ago updated_at = 1.minute.ago @@ -108,7 +108,7 @@ describe "#as_fever_json" do it "returns a hash of the story in fever format" do - feed = create_feed(name: "my feed") + feed = create(:feed, name: "my feed") published_at = 1.day.ago story = create_story( feed: feed, diff --git a/spec/repositories/feed_repository_spec.rb b/spec/repositories/feed_repository_spec.rb index e67bcfc03..2f2dac5d4 100644 --- a/spec/repositories/feed_repository_spec.rb +++ b/spec/repositories/feed_repository_spec.rb @@ -23,14 +23,13 @@ describe ".fetch_by_ids" do it "finds all feeds by id" do - feeds = [create_feed, create_feed] + feeds = create_pair(:feed) expect(FeedRepository.fetch_by_ids(feeds.map(&:id))).to match_array(feeds) end it "does not find other feeds" do - feed1 = create_feed - create_feed + feed1, = create_pair(:feed) expect(FeedRepository.fetch_by_ids(feed1.id)).to eq([feed1]) end @@ -88,7 +87,7 @@ describe ".delete" do it "deletes the feed by id" do - feed = create_feed + feed = create(:feed) FeedRepository.delete(feed.id) @@ -96,8 +95,7 @@ end it "does not delete other feeds" do - feed1 = create_feed - feed2 = create_feed + feed1, feed2 = create_pair(:feed) FeedRepository.delete(feed1.id) @@ -107,10 +105,10 @@ describe ".list" do it "returns all feeds ordered by name, case insensitive" do - feed1 = create_feed(name: "foo") - feed2 = create_feed(name: "Fabulous") - feed3 = create_feed(name: "Zooby") - feed4 = create_feed(name: "zabby") + feed1 = create(:feed, name: "foo") + feed2 = create(:feed, name: "Fabulous") + feed3 = create(:feed, name: "Zooby") + feed4 = create(:feed, name: "zabby") expect(FeedRepository.list).to eq([feed2, feed1, feed4, feed3]) end @@ -118,15 +116,14 @@ describe ".in_group" do it "returns feeds that are in a group" do - feed1 = create_feed(group_id: 5) - feed2 = create_feed(group_id: 6) + feed1 = create(:feed, group_id: 5) + feed2 = create(:feed, group_id: 6) expect(FeedRepository.in_group).to match_array([feed1, feed2]) end it "does not return feeds that are not in a group" do - create_feed - create_feed + create_pair(:feed) expect(FeedRepository.in_group).to be_empty end diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 623c3eb37..ffdd28dd0 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -119,7 +119,7 @@ describe ".fetch_unread_by_timestamp_and_group" do it "returns unread stories before timestamp for group_id" do - feed = create_feed(group_id: 52) + feed = create(:feed, group_id: 52) story = create_story(:unread, feed: feed, created_at: 5.minutes.ago) time = Time.now @@ -129,7 +129,7 @@ end it "does not return read stories before timestamp for group_id" do - feed = create_feed(group_id: 52) + feed = create(:feed, group_id: 52) create_story(feed: feed, created_at: 5.minutes.ago) time = Time.now @@ -139,7 +139,7 @@ end it "does not return unread stories after timestamp for group_id" do - feed = create_feed(group_id: 52) + feed = create(:feed, group_id: 52) create_story(:unread, feed: feed, created_at: 5.minutes.ago) time = 6.minutes.ago @@ -149,7 +149,7 @@ end it "does not return stories before timestamp for other group_id" do - feed = create_feed(group_id: 52) + feed = create(:feed, group_id: 52) create_story(:unread, feed: feed, created_at: 5.minutes.ago) time = Time.now @@ -159,7 +159,7 @@ end it "does not return stories with no group_id before timestamp" do - feed = create_feed + feed = create(:feed) create_story(:unread, feed: feed, created_at: 5.minutes.ago) time = Time.now @@ -169,7 +169,7 @@ end it "returns unread stories before timestamp for nil group_id" do - feed = create_feed + feed = create(:feed) story = create_story(:unread, feed: feed, created_at: 5.minutes.ago) time = Time.now @@ -181,7 +181,7 @@ describe ".fetch_unread_for_feed_by_timestamp" do it "returns unread stories for the feed before timestamp" do - feed = create_feed + feed = create(:feed) story = create_story(:unread, feed: feed, created_at: 5.minutes.ago) time = 4.minutes.ago @@ -192,7 +192,7 @@ end it "returns unread stories for the feed before string timestamp" do - feed = create_feed + feed = create(:feed) story = create_story(:unread, feed: feed, created_at: 5.minutes.ago) timestamp = Integer(4.minutes.ago).to_s @@ -203,7 +203,7 @@ end it "does not return read stories for the feed before timestamp" do - feed = create_feed + feed = create(:feed) create_story(feed: feed, created_at: 5.minutes.ago) time = 4.minutes.ago @@ -214,7 +214,7 @@ end it "does not return unread stories for the feed after timestamp" do - feed = create_feed + feed = create(:feed) create_story(:unread, feed: feed, created_at: 5.minutes.ago) time = 6.minutes.ago @@ -225,7 +225,7 @@ end it "does not return unread stories for another feed before timestamp" do - feed = create_feed + feed = create(:feed) create_story(:unread, created_at: 5.minutes.ago) time = 4.minutes.ago @@ -277,14 +277,14 @@ describe ".feed" do it "returns stories for the given feed id" do - feed = create_feed + feed = create(:feed) story = create_story(feed: feed) expect(StoryRepository.feed(feed.id)).to eq([story]) end it "sorts stories by published" do - feed = create_feed + feed = create(:feed) story1 = create_story(feed: feed, published: 1.day.ago) story2 = create_story(feed: feed, published: 1.hour.ago) @@ -292,7 +292,7 @@ end it "does not return stories for other feeds" do - feed = create_feed + feed = create(:feed) create_story expect(StoryRepository.feed(feed.id)).to be_empty diff --git a/spec/tasks/fetch_feeds_spec.rb b/spec/tasks/fetch_feeds_spec.rb index 8070b6988..d3ba07b18 100644 --- a/spec/tasks/fetch_feeds_spec.rb +++ b/spec/tasks/fetch_feeds_spec.rb @@ -36,7 +36,7 @@ describe "#prepare_to_delay" do it "serializes the instance for backgrounding" do - feeds = [create_feed, create_feed] + feeds = create_pair(:feed) feeds_ids = feeds.map(&:id) fetch_feeds = FetchFeeds.new(feeds) @@ -49,7 +49,7 @@ describe ".enqueue" do it "enqueues a fetch_all job" do - feeds = [create_feed, create_feed] + feeds = create_pair(:feed) feeds_ids = feeds.map(&:id) expect { FetchFeeds.enqueue(feeds) }.to change(Delayed::Job, :count).by(1) From 45bd00496d09ded422d45955144dbdcb724509f7 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 20 Dec 2022 16:03:54 -0800 Subject: [PATCH 0432/1107] Factories: switch stories to FactoryBot (#735) --- spec/commands/stories/mark_as_read_spec.rb | 2 +- spec/commands/stories/mark_as_starred_spec.rb | 2 +- spec/commands/stories/mark_as_unread_spec.rb | 2 +- .../stories/mark_as_unstarred_spec.rb | 2 +- spec/factories/stories.rb | 26 ++--- spec/models/story_spec.rb | 10 +- spec/repositories/story_repository_spec.rb | 100 +++++++++--------- 7 files changed, 73 insertions(+), 71 deletions(-) diff --git a/spec/commands/stories/mark_as_read_spec.rb b/spec/commands/stories/mark_as_read_spec.rb index c53e4bf02..7d10291ff 100644 --- a/spec/commands/stories/mark_as_read_spec.rb +++ b/spec/commands/stories/mark_as_read_spec.rb @@ -4,7 +4,7 @@ describe MarkAsRead do describe "#mark_as_read" do - let(:story) { create_story(is_read: false) } + let(:story) { create(:story, is_read: false) } it "marks a story as read" do expect { MarkAsRead.new(story.id).mark_as_read } diff --git a/spec/commands/stories/mark_as_starred_spec.rb b/spec/commands/stories/mark_as_starred_spec.rb index 1ddfa15bb..5413d80dd 100644 --- a/spec/commands/stories/mark_as_starred_spec.rb +++ b/spec/commands/stories/mark_as_starred_spec.rb @@ -4,7 +4,7 @@ describe MarkAsStarred do describe "#mark_as_starred" do - let(:story) { create_story(is_starred: false) } + let(:story) { create(:story, is_starred: false) } it "marks a story as starred" do expect { MarkAsStarred.new(story.id).mark_as_starred } diff --git a/spec/commands/stories/mark_as_unread_spec.rb b/spec/commands/stories/mark_as_unread_spec.rb index 565def114..0971ceb63 100644 --- a/spec/commands/stories/mark_as_unread_spec.rb +++ b/spec/commands/stories/mark_as_unread_spec.rb @@ -4,7 +4,7 @@ describe MarkAsUnread do describe "#mark_as_unread" do - let(:story) { create_story(is_read: true) } + let(:story) { create(:story, is_read: true) } it "marks a story as unread" do expect { MarkAsUnread.new(story.id).mark_as_unread } diff --git a/spec/commands/stories/mark_as_unstarred_spec.rb b/spec/commands/stories/mark_as_unstarred_spec.rb index de3844685..b1d06be36 100644 --- a/spec/commands/stories/mark_as_unstarred_spec.rb +++ b/spec/commands/stories/mark_as_unstarred_spec.rb @@ -4,7 +4,7 @@ describe MarkAsUnstarred do describe "#mark_as_unstarred" do - let(:story) { create_story(is_starred: true) } + let(:story) { create(:story, is_starred: true) } it "marks a story as unstarred" do expect { MarkAsUnstarred.new(story.id).mark_as_unstarred } diff --git a/spec/factories/stories.rb b/spec/factories/stories.rb index 4713b0521..11a8add66 100644 --- a/spec/factories/stories.rb +++ b/spec/factories/stories.rb @@ -1,17 +1,19 @@ -module Factories - STORY_TRAITS = { - read: -> { { is_read: true } }, - starred: -> { { is_starred: true } }, - unread: -> { { is_read: false } } - }.freeze +FactoryBot.define do + factory(:story) do + feed - def create_story(*traits, **params) - build_story(*traits, **params).tap(&:save!) - end + sequence(:entry_id, 100) { |n| "entry-#{n}" } + + trait :read do + is_read { true } + end - def build_story(*traits, **params) - traits.each { |trait| params.merge!(STORY_TRAITS.fetch(trait).call) } + trait :starred do + is_starred { true } + end - Story.new(entry_id: next_id, feed: FactoryBot.build(:feed), **params) + trait :unread do + is_read { false } + end end end diff --git a/spec/models/story_spec.rb b/spec/models/story_spec.rb index ce43ea6cf..27fb71ac0 100644 --- a/spec/models/story_spec.rb +++ b/spec/models/story_spec.rb @@ -68,7 +68,8 @@ published_at = 1.day.ago created_at = 1.hour.ago updated_at = 1.minute.ago - story = create_story( + story = create( + :story, body: "story body", created_at: created_at, entry_id: 5, @@ -110,7 +111,8 @@ it "returns a hash of the story in fever format" do feed = create(:feed, name: "my feed") published_at = 1.day.ago - story = create_story( + story = create( + :story, feed: feed, title: "the story title", body: "story body", @@ -133,12 +135,12 @@ end it "returns is_read as 0 if story is unread" do - story = create_story(is_read: false) + story = create(:story, is_read: false) expect(story.as_fever_json[:is_read]).to eq(0) end it "returns is_saved as 1 if story is starred" do - story = create_story(is_starred: true) + story = create(:story, is_starred: true) expect(story.as_fever_json[:is_saved]).to eq(1) end end diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index ffdd28dd0..816bedb6a 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -73,7 +73,7 @@ describe ".fetch" do it "finds the story by id" do - story = create_story + story = create(:story) expect(StoryRepository.fetch(story.id)).to eq(story) end @@ -81,8 +81,8 @@ describe ".fetch_by_ids" do it "finds all stories by id" do - story1 = create_story - story2 = create_story + story1 = create(:story) + story2 = create(:story) expected_stories = [story1, story2] actual_stories = StoryRepository.fetch_by_ids(expected_stories.map(&:id)) @@ -93,7 +93,7 @@ describe ".fetch_unread_by_timestamp" do it "returns unread stories from before the timestamp" do - story = create_story(created_at: 1.week.ago, is_read: false) + story = create(:story, created_at: 1.week.ago, is_read: false) actual_stories = StoryRepository.fetch_unread_by_timestamp(4.days.ago) @@ -101,7 +101,7 @@ end it "does not return unread stories from after the timestamp" do - create_story(created_at: 3.days.ago, is_read: false) + create(:story, created_at: 3.days.ago, is_read: false) actual_stories = StoryRepository.fetch_unread_by_timestamp(4.days.ago) @@ -109,7 +109,7 @@ end it "does not return read stories from before the timestamp" do - create_story(created_at: 1.week.ago, is_read: true) + create(:story, created_at: 1.week.ago, is_read: true) actual_stories = StoryRepository.fetch_unread_by_timestamp(4.days.ago) @@ -120,7 +120,7 @@ describe ".fetch_unread_by_timestamp_and_group" do it "returns unread stories before timestamp for group_id" do feed = create(:feed, group_id: 52) - story = create_story(:unread, feed: feed, created_at: 5.minutes.ago) + story = create(:story, :unread, feed: feed, created_at: 5.minutes.ago) time = Time.now stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, 52) @@ -130,7 +130,7 @@ it "does not return read stories before timestamp for group_id" do feed = create(:feed, group_id: 52) - create_story(feed: feed, created_at: 5.minutes.ago) + create(:story, feed: feed, created_at: 5.minutes.ago) time = Time.now stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, 52) @@ -140,7 +140,7 @@ it "does not return unread stories after timestamp for group_id" do feed = create(:feed, group_id: 52) - create_story(:unread, feed: feed, created_at: 5.minutes.ago) + create(:story, :unread, feed: feed, created_at: 5.minutes.ago) time = 6.minutes.ago stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, 52) @@ -150,7 +150,7 @@ it "does not return stories before timestamp for other group_id" do feed = create(:feed, group_id: 52) - create_story(:unread, feed: feed, created_at: 5.minutes.ago) + create(:story, :unread, feed: feed, created_at: 5.minutes.ago) time = Time.now stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, 55) @@ -160,7 +160,7 @@ it "does not return stories with no group_id before timestamp" do feed = create(:feed) - create_story(:unread, feed: feed, created_at: 5.minutes.ago) + create(:story, :unread, feed: feed, created_at: 5.minutes.ago) time = Time.now stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, 52) @@ -170,7 +170,7 @@ it "returns unread stories before timestamp for nil group_id" do feed = create(:feed) - story = create_story(:unread, feed: feed, created_at: 5.minutes.ago) + story = create(:story, :unread, feed: feed, created_at: 5.minutes.ago) time = Time.now stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, nil) @@ -182,7 +182,7 @@ describe ".fetch_unread_for_feed_by_timestamp" do it "returns unread stories for the feed before timestamp" do feed = create(:feed) - story = create_story(:unread, feed: feed, created_at: 5.minutes.ago) + story = create(:story, :unread, feed: feed, created_at: 5.minutes.ago) time = 4.minutes.ago stories = @@ -193,7 +193,7 @@ it "returns unread stories for the feed before string timestamp" do feed = create(:feed) - story = create_story(:unread, feed: feed, created_at: 5.minutes.ago) + story = create(:story, :unread, feed: feed, created_at: 5.minutes.ago) timestamp = Integer(4.minutes.ago).to_s stories = @@ -204,7 +204,7 @@ it "does not return read stories for the feed before timestamp" do feed = create(:feed) - create_story(feed: feed, created_at: 5.minutes.ago) + create(:story, feed: feed, created_at: 5.minutes.ago) time = 4.minutes.ago stories = @@ -215,7 +215,7 @@ it "does not return unread stories for the feed after timestamp" do feed = create(:feed) - create_story(:unread, feed: feed, created_at: 5.minutes.ago) + create(:story, :unread, feed: feed, created_at: 5.minutes.ago) time = 6.minutes.ago stories = @@ -226,7 +226,7 @@ it "does not return unread stories for another feed before timestamp" do feed = create(:feed) - create_story(:unread, created_at: 5.minutes.ago) + create(:story, :unread, created_at: 5.minutes.ago) time = 4.minutes.ago stories = @@ -238,15 +238,15 @@ describe ".unread" do it "returns unread stories ordered by published date descending" do - story1 = create_story(:unread, published: 5.minutes.ago) - story2 = create_story(:unread, published: 4.minutes.ago) + story1 = create(:story, :unread, published: 5.minutes.ago) + story2 = create(:story, :unread, published: 4.minutes.ago) expect(StoryRepository.unread).to eq([story2, story1]) end it "does not return read stories" do - create_story(published: 5.minutes.ago) - create_story(published: 4.minutes.ago) + create(:story, published: 5.minutes.ago) + create(:story, published: 4.minutes.ago) expect(StoryRepository.unread).to be_empty end @@ -254,22 +254,22 @@ describe ".unread_since_id" do it "returns unread stories with id greater than given id" do - story1 = create_story(:unread) - story2 = create_story(:unread) + story1 = create(:story, :unread) + story2 = create(:story, :unread) expect(StoryRepository.unread_since_id(story1.id)).to eq([story2]) end it "does not return read stories with id greater than given id" do - story1 = create_story(:unread) - create_story + story1 = create(:story, :unread) + create(:story) expect(StoryRepository.unread_since_id(story1.id)).to be_empty end it "does not return unread stories with id less than given id" do - create_story(:unread) - story2 = create_story(:unread) + create(:story, :unread) + story2 = create(:story, :unread) expect(StoryRepository.unread_since_id(story2.id)).to be_empty end @@ -278,22 +278,22 @@ describe ".feed" do it "returns stories for the given feed id" do feed = create(:feed) - story = create_story(feed: feed) + story = create(:story, feed: feed) expect(StoryRepository.feed(feed.id)).to eq([story]) end it "sorts stories by published" do feed = create(:feed) - story1 = create_story(feed: feed, published: 1.day.ago) - story2 = create_story(feed: feed, published: 1.hour.ago) + story1 = create(:story, feed: feed, published: 1.day.ago) + story2 = create(:story, feed: feed, published: 1.hour.ago) expect(StoryRepository.feed(feed.id)).to eq([story2, story1]) end it "does not return stories for other feeds" do feed = create(:feed) - create_story + create(:story) expect(StoryRepository.feed(feed.id)).to be_empty end @@ -301,27 +301,27 @@ describe ".read" do it "returns read stories" do - story = create_story(:read) + story = create(:story, :read) expect(StoryRepository.read).to eq([story]) end it "sorts stories by published" do - story1 = create_story(:read, published: 1.day.ago) - story2 = create_story(:read, published: 1.hour.ago) + story1 = create(:story, :read, published: 1.day.ago) + story2 = create(:story, :read, published: 1.hour.ago) expect(StoryRepository.read).to eq([story2, story1]) end it "does not return unread stories" do - create_story(:unread) + create(:story, :unread) expect(StoryRepository.read).to be_empty end it "paginates results" do stories = - 21.times.map { |num| create_story(:read, published: num.days.ago) } + 21.times.map { |num| create(:story, :read, published: num.days.ago) } expect(StoryRepository.read).to eq(stories[0...20]) expect(StoryRepository.read(2)).to eq([stories.last]) @@ -330,27 +330,27 @@ describe ".starred" do it "returns starred stories" do - story = create_story(:starred) + story = create(:story, :starred) expect(StoryRepository.starred).to eq([story]) end it "sorts stories by published" do - story1 = create_story(:starred, published: 1.day.ago) - story2 = create_story(:starred, published: 1.hour.ago) + story1 = create(:story, :starred, published: 1.day.ago) + story2 = create(:story, :starred, published: 1.hour.ago) expect(StoryRepository.starred).to eq([story2, story1]) end it "does not return unstarred stories" do - create_story + create(:story) expect(StoryRepository.starred).to be_empty end it "paginates results" do stories = - 21.times.map { |num| create_story(:starred, published: num.days.ago) } + 21.times.map { |num| create(:story, :starred, published: num.days.ago) } expect(StoryRepository.starred).to eq(stories[0...20]) expect(StoryRepository.starred(2)).to eq([stories.last]) @@ -359,26 +359,26 @@ describe ".unstarred_read_stories_older_than" do it "returns unstarred read stories older than given number of days" do - story = create_story(:read, published: 6.days.ago) + story = create(:story, :read, published: 6.days.ago) expect(StoryRepository.unstarred_read_stories_older_than(5)) .to eq([story]) end it "does not return starred stories older than the given number of days" do - create_story(:read, :starred, published: 6.days.ago) + create(:story, :read, :starred, published: 6.days.ago) expect(StoryRepository.unstarred_read_stories_older_than(5)).to be_empty end it "does not return unread stories older than the given number of days" do - create_story(:unread, published: 6.days.ago) + create(:story, :unread, published: 6.days.ago) expect(StoryRepository.unstarred_read_stories_older_than(5)).to be_empty end it "does not return stories newer than given number of days" do - create_story(:read, published: 4.days.ago) + create(:story, :read, published: 4.days.ago) expect(StoryRepository.unstarred_read_stories_older_than(5)).to be_empty end @@ -386,17 +386,15 @@ describe ".read_count" do it "returns the count of read stories" do - create_story(:read) - create_story(:read) - create_story(:read) + create(:story, :read) + create(:story, :read) + create(:story, :read) expect(StoryRepository.read_count).to eq(3) end it "does not count unread stories" do - create_story(:unread) - create_story(:unread) - create_story(:unread) + create_list(:story, 3, :unread) expect(StoryRepository.read_count).to eq(0) end From 7681b79a37f47b5873fece07a80f47cd1a1e993e Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 20 Dec 2022 16:23:50 -0800 Subject: [PATCH 0433/1107] Factories: switch groups to FactoryBot (#736) --- spec/factories/groups.rb | 10 ++-------- spec/repositories/group_repository_spec.rb | 8 ++++---- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index 9d7ba4e85..3feda95ef 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -1,9 +1,3 @@ -module Factories - def create_group(params = {}) - build_group(params).tap(&:save!) - end - - def build_group(params = {}) - Group.new(**params) - end +FactoryBot.define do + factory(:group) end diff --git a/spec/repositories/group_repository_spec.rb b/spec/repositories/group_repository_spec.rb index b1ab5c268..fd119fa27 100644 --- a/spec/repositories/group_repository_spec.rb +++ b/spec/repositories/group_repository_spec.rb @@ -6,10 +6,10 @@ describe GroupRepository do describe ".list" do it "lists groups ordered by lower name" do - group1 = create_group(name: "Zabba") - group2 = create_group(name: "zlabba") - group3 = create_group(name: "blabba") - group4 = create_group(name: "Babba") + group1 = create(:group, name: "Zabba") + group2 = create(:group, name: "zlabba") + group3 = create(:group, name: "blabba") + group4 = create(:group, name: "Babba") expected_groups = [group4, group3, group1, group2] expect(GroupRepository.list).to eq(expected_groups) From e4f08b0d39e6f7acb24dada11b9cfdad8e76456f Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 20 Dec 2022 16:42:52 -0800 Subject: [PATCH 0434/1107] Factories: switch GroupFactory to FactoryBot (#737) --- .rubocop_todo.yml | 4 ---- spec/factories.rb | 1 - spec/factories/group_factory.rb | 17 ----------------- spec/fever_api_spec.rb | 2 +- 4 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 spec/factories/group_factory.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 113d91945..285089a8b 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -799,7 +799,6 @@ Style/FrozenStringLiteralComment: - 'spec/factories.rb' - 'spec/factories/feed_factory.rb' - 'spec/factories/feeds.rb' - - 'spec/factories/group_factory.rb' - 'spec/factories/groups.rb' - 'spec/factories/stories.rb' - 'spec/factories/story_factory.rb' @@ -864,7 +863,6 @@ Style/HashSyntax: - 'app/repositories/story_repository.rb' - 'app/utils/sample_story.rb' - 'spec/factories/feed_factory.rb' - - 'spec/factories/group_factory.rb' - 'spec/factories/story_factory.rb' - 'spec/fever_api/read_feeds_groups_spec.rb' - 'spec/fever_api/read_feeds_spec.rb' @@ -957,7 +955,6 @@ Style/OpenStructUse: Exclude: - 'app.rb' - 'spec/factories/feed_factory.rb' - - 'spec/factories/group_factory.rb' - 'spec/factories/story_factory.rb' # Offense count: 28 @@ -979,7 +976,6 @@ Style/OptionHash: - 'app/fever_api/write_mark_item.rb' - 'spec/factories/feed_factory.rb' - 'spec/factories/feeds.rb' - - 'spec/factories/group_factory.rb' - 'spec/factories/groups.rb' - 'spec/factories/story_factory.rb' diff --git a/spec/factories.rb b/spec/factories.rb index 6b2ba18e4..762511d70 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,6 +1,5 @@ require_relative "factories/feed_factory" require_relative "factories/story_factory" -require_relative "factories/group_factory" require_relative "factories/feeds" require_relative "factories/groups" require_relative "factories/stories" diff --git a/spec/factories/group_factory.rb b/spec/factories/group_factory.rb deleted file mode 100644 index 58814dab5..000000000 --- a/spec/factories/group_factory.rb +++ /dev/null @@ -1,17 +0,0 @@ -class GroupFactory - class FakeGroup < OpenStruct - def as_fever_json - { - id: id, - title: name - } - end - end - - def self.build(params = {}) - FakeGroup.new( - id: rand(100), - name: params[:name] || "#{Faker::Name.name} group" - ) - end -end diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index 4c3bef019..7d643fbe2 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -11,7 +11,7 @@ def app let(:api_key) { "apisecretkey" } let(:story_one) { StoryFactory.build } let(:story_two) { StoryFactory.build } - let(:group) { GroupFactory.build } + let(:group) { build(:group) } let(:feed) { FeedFactory.build(group_id: group.id) } let(:stories) { [story_one, story_two] } let(:standard_answer) do From 39a6a8859fd64426e1d5eae742e371d76e15bf53 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 20 Dec 2022 17:26:09 -0800 Subject: [PATCH 0435/1107] Factories: replace StoryFactory with FactoryBot (#738) --- .rubocop_todo.yml | 7 ---- spec/controllers/stories_controller_spec.rb | 12 +++--- spec/factories.rb | 1 - spec/factories/feeds.rb | 1 + spec/factories/stories.rb | 2 + spec/factories/story_factory.rb | 41 --------------------- spec/fever_api_spec.rb | 4 +- spec/support/factory_bot.rb | 1 + 8 files changed, 12 insertions(+), 57 deletions(-) delete mode 100644 spec/factories/story_factory.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 285089a8b..0f666e620 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -22,7 +22,6 @@ Lint/NumberConversion: - 'app/models/story.rb' - 'app/repositories/story_repository.rb' - 'spec/factories/feed_factory.rb' - - 'spec/factories/story_factory.rb' - 'spec/models/feed_spec.rb' - 'spec/models/story_spec.rb' @@ -47,7 +46,6 @@ Metrics/MethodLength: - 'app/utils/sample_story.rb' - 'config/asset_pipeline.rb' - 'db/migrate/20130425222157_add_delayed_job.rb' - - 'spec/factories/story_factory.rb' # Offense count: 9 # Configuration parameters: ForbiddenDelimiters. @@ -629,7 +627,6 @@ Rails/TimeZone: - 'app/utils/sample_story.rb' - 'spec/commands/find_new_stories_spec.rb' - 'spec/factories/feed_factory.rb' - - 'spec/factories/story_factory.rb' - 'spec/fever_api_spec.rb' - 'spec/integration/feed_importing_spec.rb' - 'spec/models/story_spec.rb' @@ -801,7 +798,6 @@ Style/FrozenStringLiteralComment: - 'spec/factories/feeds.rb' - 'spec/factories/groups.rb' - 'spec/factories/stories.rb' - - 'spec/factories/story_factory.rb' - 'spec/factories/users.rb' - 'spec/fever_api/authentication_spec.rb' - 'spec/fever_api/read_favicons_spec.rb' @@ -863,7 +859,6 @@ Style/HashSyntax: - 'app/repositories/story_repository.rb' - 'app/utils/sample_story.rb' - 'spec/factories/feed_factory.rb' - - 'spec/factories/story_factory.rb' - 'spec/fever_api/read_feeds_groups_spec.rb' - 'spec/fever_api/read_feeds_spec.rb' - 'spec/fever_api/read_groups_spec.rb' @@ -955,7 +950,6 @@ Style/OpenStructUse: Exclude: - 'app.rb' - 'spec/factories/feed_factory.rb' - - 'spec/factories/story_factory.rb' # Offense count: 28 # Configuration parameters: SuspiciousParamNames, Allowlist. @@ -977,7 +971,6 @@ Style/OptionHash: - 'spec/factories/feed_factory.rb' - 'spec/factories/feeds.rb' - 'spec/factories/groups.rb' - - 'spec/factories/story_factory.rb' # Offense count: 12 # This cop supports safe autocorrection (--autocorrect). diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index 47a668d5f..88e4a42fc 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -4,8 +4,8 @@ app_require "controllers/sinatra/stories_controller" describe "StoriesController" do - let(:story_one) { StoryFactory.build } - let(:story_two) { StoryFactory.build } + let(:story_one) { create(:story) } + let(:story_two) { create(:story) } let(:stories) { [story_one, story_two] } describe "GET /news" do @@ -60,8 +60,8 @@ end describe "GET /archive" do - let(:read_one) { StoryFactory.build(is_read: true) } - let(:read_two) { StoryFactory.build(is_read: true) } + let(:read_one) { build(:story, :read) } + let(:read_two) { build(:story, :read) } let(:stories) { [read_one, read_two].paginate } before { allow(StoryRepository).to receive(:read).and_return(stories) } @@ -75,8 +75,8 @@ end describe "GET /starred" do - let(:starred_one) { StoryFactory.build(is_starred: true) } - let(:starred_two) { StoryFactory.build(is_starred: true) } + let(:starred_one) { build(:story, :starred) } + let(:starred_two) { build(:story, :starred) } let(:stories) { [starred_one, starred_two].paginate } before { allow(StoryRepository).to receive(:starred).and_return(stories) } diff --git a/spec/factories.rb b/spec/factories.rb index 762511d70..e7de0de90 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,5 +1,4 @@ require_relative "factories/feed_factory" -require_relative "factories/story_factory" require_relative "factories/feeds" require_relative "factories/groups" require_relative "factories/stories" diff --git a/spec/factories/feeds.rb b/spec/factories/feeds.rb index 8fd25e7aa..a74858e7e 100644 --- a/spec/factories/feeds.rb +++ b/spec/factories/feeds.rb @@ -1,5 +1,6 @@ FactoryBot.define do factory(:feed) do + sequence(:name, 100) { |n| "Feed #{n}" } sequence(:url, 100) { |n| "http://exampoo.com/#{n}" } end end diff --git a/spec/factories/stories.rb b/spec/factories/stories.rb index 11a8add66..22859cbb1 100644 --- a/spec/factories/stories.rb +++ b/spec/factories/stories.rb @@ -4,6 +4,8 @@ sequence(:entry_id, 100) { |n| "entry-#{n}" } + published { Time.zone.now } + trait :read do is_read { true } end diff --git a/spec/factories/story_factory.rb b/spec/factories/story_factory.rb deleted file mode 100644 index 0b4557465..000000000 --- a/spec/factories/story_factory.rb +++ /dev/null @@ -1,41 +0,0 @@ -require_relative "./feed_factory" - -class StoryFactory - class FakeStory < OpenStruct - def headline - title[0, 50] - end - - def source - feed.name - end - - def as_fever_json - { - id: id, - feed_id: feed_id, - title: title, - author: source, - html: body, - url: permalink, - is_saved: is_starred ? 1 : 0, - is_read: is_read ? 1 : 0, - created_on_time: published.to_i - } - end - end - - def self.build(params = {}) - default_params = { - id: rand(100), - title: Faker::Lorem.sentence, - permalink: Faker::Internet.url, - body: Faker::Lorem.paragraph, - feed: FeedFactory.build, - is_read: false, - is_starred: false, - published: Time.now - } - FakeStory.new(default_params.merge(params)) - end -end diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index 7d643fbe2..8f9a57c3f 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -9,8 +9,8 @@ def app end let(:api_key) { "apisecretkey" } - let(:story_one) { StoryFactory.build } - let(:story_two) { StoryFactory.build } + let(:story_one) { build(:story) } + let(:story_two) { build(:story) } let(:group) { build(:group) } let(:feed) { FeedFactory.build(group_id: group.id) } let(:stories) { [story_one, story_two] } diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb index 84cf4a0c8..47ebafcec 100644 --- a/spec/support/factory_bot.rb +++ b/spec/support/factory_bot.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "factory_bot" +require "support/active_record" module FactoryCache def self.user From cd32ec3294b1306cf4812f496509d22651a95575 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 20 Dec 2022 17:41:09 -0800 Subject: [PATCH 0436/1107] Factories: replace FeedFactory with FactoryBot (#739) --- .rubocop_todo.yml | 13 ---------- spec/commands/feeds/add_new_feed_spec.rb | 2 +- spec/commands/feeds/export_to_opml_spec.rb | 4 ++-- spec/controllers/feeds_controller_spec.rb | 6 ++--- spec/factories.rb | 1 - spec/factories/feed_factory.rb | 28 ---------------------- spec/fever_api_spec.rb | 2 +- spec/tasks/fetch_feeds_spec.rb | 2 +- 8 files changed, 8 insertions(+), 50 deletions(-) delete mode 100644 spec/factories/feed_factory.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 0f666e620..96b279aad 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -21,7 +21,6 @@ Lint/NumberConversion: - 'app/models/feed.rb' - 'app/models/story.rb' - 'app/repositories/story_repository.rb' - - 'spec/factories/feed_factory.rb' - 'spec/models/feed_spec.rb' - 'spec/models/story_spec.rb' @@ -626,7 +625,6 @@ Rails/TimeZone: - 'app/tasks/remove_old_stories.rb' - 'app/utils/sample_story.rb' - 'spec/commands/find_new_stories_spec.rb' - - 'spec/factories/feed_factory.rb' - 'spec/fever_api_spec.rb' - 'spec/integration/feed_importing_spec.rb' - 'spec/models/story_spec.rb' @@ -794,7 +792,6 @@ Style/FrozenStringLiteralComment: - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/factories.rb' - - 'spec/factories/feed_factory.rb' - 'spec/factories/feeds.rb' - 'spec/factories/groups.rb' - 'spec/factories/stories.rb' @@ -858,7 +855,6 @@ Style/HashSyntax: - 'app/models/story.rb' - 'app/repositories/story_repository.rb' - 'app/utils/sample_story.rb' - - 'spec/factories/feed_factory.rb' - 'spec/fever_api/read_feeds_groups_spec.rb' - 'spec/fever_api/read_feeds_spec.rb' - 'spec/fever_api/read_groups_spec.rb' @@ -949,7 +945,6 @@ Style/NumericLiterals: Style/OpenStructUse: Exclude: - 'app.rb' - - 'spec/factories/feed_factory.rb' # Offense count: 28 # Configuration parameters: SuspiciousParamNames, Allowlist. @@ -968,7 +963,6 @@ Style/OptionHash: - 'app/fever_api/write_mark_feed.rb' - 'app/fever_api/write_mark_group.rb' - 'app/fever_api/write_mark_item.rb' - - 'spec/factories/feed_factory.rb' - 'spec/factories/feeds.rb' - 'spec/factories/groups.rb' @@ -1016,13 +1010,6 @@ Style/StaticClass: - 'app/utils/api_key.rb' - 'app/utils/content_sanitizer.rb' -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Mode. -Style/StringConcatenation: - Exclude: - - 'spec/factories/feed_factory.rb' - # Offense count: 19 # This cop supports unsafe autocorrection (--autocorrect-all). Style/StringHashKeys: diff --git a/spec/commands/feeds/add_new_feed_spec.rb b/spec/commands/feeds/add_new_feed_spec.rb index 3cc19ee65..52545fab2 100644 --- a/spec/commands/feeds/add_new_feed_spec.rb +++ b/spec/commands/feeds/add_new_feed_spec.rb @@ -17,7 +17,7 @@ let(:feed_url) { "http://feed.com/atom.xml" } let(:feed_result) { double(title: feed.name, feed_url: feed.url) } let(:discoverer) { double(discover: feed_result) } - let(:feed) { FeedFactory.build } + let(:feed) { build(:feed) } let(:repo) { double } it "parses and creates the feed if discovered" do diff --git a/spec/commands/feeds/export_to_opml_spec.rb b/spec/commands/feeds/export_to_opml_spec.rb index cb4c231db..259fa09e7 100644 --- a/spec/commands/feeds/export_to_opml_spec.rb +++ b/spec/commands/feeds/export_to_opml_spec.rb @@ -4,8 +4,8 @@ describe ExportToOpml do describe "#to_xml" do - let(:feed_one) { FeedFactory.build } - let(:feed_two) { FeedFactory.build } + let(:feed_one) { build(:feed) } + let(:feed_two) { build(:feed) } let(:feeds) { [feed_one, feed_two] } it "returns OPML XML" do diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index 170cb3373..574f3c729 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -3,7 +3,7 @@ app_require "controllers/feeds_controller" describe "FeedsController" do - let(:feeds) { [FeedFactory.build, FeedFactory.build] } + let(:feeds) { build_pair(:feed) } describe "GET /feeds" do it "renders a list of feeds" do @@ -60,7 +60,7 @@ def params(feed, **overrides) describe "PUT /feeds/:feed_id" do it "updates a feed given the id" do - feed = FeedFactory.build(url: "example.com/atom", id: "12", group_id: nil) + feed = build(:feed, url: "example.com/atom", id: "12", group_id: nil) mock_feed(feed, "Test", "example.com/feed") feed_url = "example.com/feed" @@ -70,7 +70,7 @@ def params(feed, **overrides) end it "updates a feed group given the id" do - feed = FeedFactory.build(url: "example.com/atom") + feed = build(:feed, url: "example.com/atom") mock_feed(feed, feed.name, feed.url, "321") put "/feeds/123", **params(feed, feed_id: "123", group_id: "321") diff --git a/spec/factories.rb b/spec/factories.rb index e7de0de90..eb39ac32f 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,4 +1,3 @@ -require_relative "factories/feed_factory" require_relative "factories/feeds" require_relative "factories/groups" require_relative "factories/stories" diff --git a/spec/factories/feed_factory.rb b/spec/factories/feed_factory.rb deleted file mode 100644 index b649894a6..000000000 --- a/spec/factories/feed_factory.rb +++ /dev/null @@ -1,28 +0,0 @@ -class FeedFactory - class FakeFeed < OpenStruct - def as_fever_json - { - id: id, - favicon_id: 0, - title: name, - url: url, - site_url: url, - is_spark: 0, - last_updated_on_time: last_fetched.to_i - } - end - end - - def self.build(params = {}) - FakeFeed.new( - id: rand(100), - group_id: rand(100), - name: Faker::Name.name + " on Software", - url: Faker::Internet.url, - last_fetched: Time.now, - stories: [], - unread_stories: [], - **params - ) - end -end diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index 8f9a57c3f..914f4bc5e 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -12,7 +12,7 @@ def app let(:story_one) { build(:story) } let(:story_two) { build(:story) } let(:group) { build(:group) } - let(:feed) { FeedFactory.build(group_id: group.id) } + let(:feed) { build(:feed, group: group) } let(:stories) { [story_one, story_two] } let(:standard_answer) do { api_version: 3, auth: 1, last_refreshed_on_time: 123456789 } diff --git a/spec/tasks/fetch_feeds_spec.rb b/spec/tasks/fetch_feeds_spec.rb index d3ba07b18..7b66229ad 100644 --- a/spec/tasks/fetch_feeds_spec.rb +++ b/spec/tasks/fetch_feeds_spec.rb @@ -3,7 +3,7 @@ describe FetchFeeds do describe "#fetch_all" do - let(:feeds) { [FeedFactory.build, FeedFactory.build] } + let(:feeds) { build_pair(:feed) } let(:fetcher_one) { instance_double(FetchFeed) } let(:fetcher_two) { instance_double(FetchFeed) } let(:pool) { double } From 2a96941f21fd3fcbebd8023d3ab3192d67519fd3 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 20 Dec 2022 17:47:50 -0800 Subject: [PATCH 0437/1107] Factories: remove Factories module (#740) --- spec/factories.rb | 7 ------- spec/spec_helper.rb | 1 - 2 files changed, 8 deletions(-) diff --git a/spec/factories.rb b/spec/factories.rb index eb39ac32f..49b48b372 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -2,10 +2,3 @@ require_relative "factories/groups" require_relative "factories/stories" require_relative "factories/users" - -module Factories - def next_id - @next_id ||= 0 - @next_id += 1 - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6bfd9b0b5..6873a783f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -35,7 +35,6 @@ def custom_request(method, path, params = {}, env = {}, &) RSpec.configure do |config| config.include Rack::Test::Methods config.include RSpecHtmlMatchers - config.include Factories end def app_require(file) From 5dd4011d2d23000de9fbb0b0a34809349546e41b Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Tue, 27 Dec 2022 20:41:01 -0800 Subject: [PATCH 0438/1107] Update all Bundler dependencies (2022-12-26) (#742) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- Gemfile.lock | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a8c143966..4c244265e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -83,11 +83,11 @@ GEM xpath (~> 3.2) coderay (1.1.3) concurrent-ruby (1.1.10) - coveralls_reborn (0.25.0) - simplecov (>= 0.18.1, < 0.22.0) - term-ansicolor (~> 1.6) - thor (>= 0.20.3, < 2.0) - tins (~> 1.16) + coveralls_reborn (0.26.0) + simplecov (~> 0.22.0) + term-ansicolor (~> 1.7) + thor (~> 1.2) + tins (~> 1.32) crass (1.0.6) date (3.3.3) delayed_job (4.1.11) @@ -97,14 +97,14 @@ GEM delayed_job (>= 3.0, < 5) diff-lcs (1.5.0) docile (1.4.0) - erubi (1.11.0) + erubi (1.12.0) execjs (2.8.1) factory_bot (6.2.1) activesupport (>= 5.0.0) factory_bot_rails (6.2.0) factory_bot (~> 6.2.0) railties (>= 5.0.0) - faker (3.0.0) + faker (3.1.0) i18n (>= 1.8.11, < 2) feedbag (1.0.0) nokogiri (~> 1.8, >= 1.8.2) @@ -135,13 +135,13 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) mini_mime (1.1.2) - mini_portile2 (2.8.0) + mini_portile2 (2.8.1) minitest (5.16.3) multi_json (1.15.0) multi_xml (0.6.0) mustermann (3.0.0) ruby2_keywords (~> 0.0.1) - net-imap (0.3.2) + net-imap (0.3.4) date net-protocol net-pop (0.1.2) @@ -165,9 +165,9 @@ GEM byebug (~> 11.0) pry (>= 0.13, < 0.15) public_suffix (5.0.1) - puma (6.0.0) + puma (6.0.1) nio4r (~> 2.0) - racc (1.6.1) + racc (1.6.2) rack (2.2.4) rack-protection (3.0.5) rack @@ -235,7 +235,7 @@ GEM rspec-mocks (~> 3.11) rspec-support (~> 3.11) rspec-support (3.12.0) - rubocop (1.40.0) + rubocop (1.41.1) json (~> 2.3) parallel (~> 1.10) parser (>= 3.1.2.1) @@ -247,7 +247,7 @@ GEM unicode-display_width (>= 1.4.0, < 3.0) rubocop-ast (1.24.0) parser (>= 3.1.1.0) - rubocop-rails (2.17.3) + rubocop-rails (2.17.4) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) @@ -265,7 +265,7 @@ GEM sax-machine (1.3.2) shotgun (0.9.2) rack (>= 1.0) - simplecov (0.21.2) + simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) @@ -287,9 +287,9 @@ GEM tilt (~> 2.0) sinatra-flash (0.3.0) sinatra (>= 1.0.0) - sprockets (4.1.1) + sprockets (4.2.0) concurrent-ruby (~> 1.0) - rack (> 1, < 3) + rack (>= 2.2.4, < 4) sprockets-helpers (1.4.0) sprockets (>= 2.2) sync (0.5.0) From 96e28976a7cab7e317a2097ae56d57762104c843 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Wed, 28 Dec 2022 14:11:23 -0800 Subject: [PATCH 0439/1107] RuboCop: switch WordArray rule (#745) Sticking with brackets for consistency. --- .rubocop.yml | 2 ++ .rubocop_todo.yml | 13 ------------- app/helpers/authentication_helpers.rb | 2 +- app/helpers/url_helpers.rb | 2 +- config/asset_pipeline.rb | 2 +- spec/controllers/stories_controller_spec.rb | 2 +- spec/fever_api/read_items_spec.rb | 2 +- spec/helpers/authentications_helper_spec.rb | 4 ++-- spec/helpers/url_helpers_spec.rb | 2 +- spec/javascript/test_controller.rb | 20 ++++++++++---------- 10 files changed, 20 insertions(+), 31 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 824da5e97..84149d247 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -23,6 +23,8 @@ Style/StringLiterals: { EnforcedStyle: double_quotes } # want to enable these, but they don't work right when using `.rubocop_todo.yml` Style/DocumentationMethod: { Enabled: false } Style/Documentation: { Enabled: false } +Style/SymbolArray: { EnforcedStyle: brackets } +Style/WordArray: { EnforcedStyle: brackets } ################################################################################ # diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 96b279aad..24626f36f 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -966,19 +966,6 @@ Style/OptionHash: - 'spec/factories/feeds.rb' - 'spec/factories/groups.rb' -# Offense count: 12 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: PreferredDelimiters. -Style/PercentLiteralDelimiters: - Exclude: - - 'app/helpers/authentication_helpers.rb' - - 'app/helpers/url_helpers.rb' - - 'spec/controllers/stories_controller_spec.rb' - - 'spec/fever_api/read_items_spec.rb' - - 'spec/helpers/authentications_helper_spec.rb' - - 'spec/helpers/url_helpers_spec.rb' - - 'spec/javascript/test_controller.rb' - # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. diff --git a/app/helpers/authentication_helpers.rb b/app/helpers/authentication_helpers.rb index cb70a94f5..93e0eb364 100644 --- a/app/helpers/authentication_helpers.rb +++ b/app/helpers/authentication_helpers.rb @@ -10,7 +10,7 @@ def authenticated? def needs_authentication?(path) return false unless UserRepository.setup_complete? - return false if %w(/login /logout /heroku).include?(path) + return false if ["/login", "/logout", "/heroku"].include?(path) return false if path =~ /css|js|img/ true diff --git a/app/helpers/url_helpers.rb b/app/helpers/url_helpers.rb index 0be9dd386..b9e0121f9 100644 --- a/app/helpers/url_helpers.rb +++ b/app/helpers/url_helpers.rb @@ -7,7 +7,7 @@ module UrlHelpers def expand_absolute_urls(content, base_url) doc = Nokogiri::HTML.fragment(content) - [%w(a href), %w(img src), %w(video src)].each do |tag, attr| + [["a", "href"], ["img", "src"], ["video", "src"]].each do |tag, attr| doc.css("#{tag}[#{attr}]").each do |node| url = node.get_attribute(attr) next if url =~ ABS_RE diff --git a/config/asset_pipeline.rb b/config/asset_pipeline.rb index 32f2b2a83..59b009e36 100644 --- a/config/asset_pipeline.rb +++ b/config/asset_pipeline.rb @@ -2,7 +2,7 @@ module AssetPipeline def registered(app) app.set :sprockets, Sprockets::Environment.new(app.root) - %w[assets stylesheets javascripts].each do |path| + ["assets", "stylesheets", "javascripts"].each do |path| app.get "/#{path}/*" do env["PATH_INFO"].sub!(%r{^/#{path}}, "") settings.sprockets.call(env) diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index 88e4a42fc..0fcf3fa24 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -154,7 +154,7 @@ it "marks all unread stories as read and reload the page" do expect_any_instance_of(MarkAllAsRead).to receive(:mark_as_read).once - post "/stories/mark_all_as_read", story_ids: %w(1 2 3) + post "/stories/mark_all_as_read", story_ids: ["1", "2", "3"] expect(last_response.status).to be 302 expect(URI.parse(last_response.location).path).to eq "/news" diff --git a/spec/fever_api/read_items_spec.rb b/spec/fever_api/read_items_spec.rb index fef8e3131..b56eca152 100644 --- a/spec/fever_api/read_items_spec.rb +++ b/spec/fever_api/read_items_spec.rb @@ -54,7 +54,7 @@ double("story", as_fever_json: { id: 11 }) ] expect(story_repository) - .to receive(:fetch_by_ids).with(%w(5 11)).twice.and_return(stories) + .to receive(:fetch_by_ids).with(["5", "11"]).twice.and_return(stories) expect(subject.call("items" => nil, with_ids: "5,11")).to eq( items: [ diff --git a/spec/helpers/authentications_helper_spec.rb b/spec/helpers/authentications_helper_spec.rb index e66496e00..7d77141df 100644 --- a/spec/helpers/authentications_helper_spec.rb +++ b/spec/helpers/authentications_helper_spec.rb @@ -23,7 +23,7 @@ end end - %w(/login /logout /heroku).each do |path| + ["/login", "/logout", "/heroku"].each do |path| context "when `path` is '#{path}'" do it "returns false" do expect(helper.needs_authentication?(path)).to be(false) @@ -31,7 +31,7 @@ end end - %w(css js img).each do |path| + ["css", "js", "img"].each do |path| context "when `path` contains '#{path}'" do it "returns false" do expect(helper.needs_authentication?("/#{path}/file.ext")).to be(false) diff --git a/spec/helpers/url_helpers_spec.rb b/spec/helpers/url_helpers_spec.rb index 7a422d2ee..edcb36d63 100644 --- a/spec/helpers/url_helpers_spec.rb +++ b/spec/helpers/url_helpers_spec.rb @@ -77,7 +77,7 @@ describe "#normalize_url" do it "resolves scheme-less urls" do - %w(http https).each do |scheme| + ["http", "https"].each do |scheme| feed_url = "#{scheme}://blog.golang.org/feed.atom" url = helper.normalize_url("//blog.golang.org/context", feed_url) diff --git a/spec/javascript/test_controller.rb b/spec/javascript/test_controller.rb index 97c62a491..cb6025913 100644 --- a/spec/javascript/test_controller.rb +++ b/spec/javascript/test_controller.rb @@ -23,26 +23,26 @@ def self.test_path(*chunks) private def vendor_js_files - %w( - mocha.js - sinon.js - chai.js - chai-changes.js - chai-backbone.js - sinon-chai.js - ).map do |name| + [ + "mocha.js", + "sinon.js", + "chai.js", + "chai-changes.js", + "chai-backbone.js", + "sinon-chai.js" + ].map do |name| File.join "vendor", "js", name end end def vendor_css_files - %w(mocha.css).map do |name| + ["mocha.css"].map do |name| File.join "vendor", "css", name end end def js_helper_files - %w(spec_helper.js).map do |name| + ["spec_helper.js"].map do |name| File.join "spec", name end end From c92b43d3ad69d5459e488893c071d7bb5fd1c1d8 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Wed, 28 Dec 2022 14:53:00 -0800 Subject: [PATCH 0440/1107] Fix: handle feeds that are missing a title (#746) Use the URL in its place. Fixes #744. --- .rubocop_todo.yml | 1 + app/commands/feeds/add_new_feed.rb | 8 +++----- app/controllers/feeds_controller.rb | 4 ++++ spec/commands/feeds/add_new_feed_spec.rb | 11 +++++++++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 24626f36f..fc8c0b673 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -216,6 +216,7 @@ RSpec/EmptyLineAfterHook: # Configuration parameters: Max, CountAsOne. RSpec/ExampleLength: Exclude: + - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/commands/feeds/export_to_opml_spec.rb' - 'spec/commands/find_new_stories_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' diff --git a/app/commands/feeds/add_new_feed.rb b/app/commands/feeds/add_new_feed.rb index 5fa5810db..0b13f2eb8 100644 --- a/app/commands/feeds/add_new_feed.rb +++ b/app/commands/feeds/add_new_feed.rb @@ -9,10 +9,8 @@ def self.add(url, discoverer = FeedDiscovery.new, repo = Feed) result = discoverer.discover(url) return false unless result - repo.create( - name: ContentSanitizer.sanitize(result.title), - url: result.feed_url, - last_fetched: Time.now - ONE_DAY - ) + name = ContentSanitizer.sanitize(result.title.presence || result.feed_url) + + repo.create(name:, url: result.feed_url, last_fetched: Time.now - ONE_DAY) end end diff --git a/app/controllers/feeds_controller.rb b/app/controllers/feeds_controller.rb index 0f1488707..298cea139 100644 --- a/app/controllers/feeds_controller.rb +++ b/app/controllers/feeds_controller.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +require_relative "../repositories/feed_repository" +require_relative "../commands/feeds/add_new_feed" +require_relative "../commands/feeds/export_to_opml" + class FeedsController < ApplicationController def index @feeds = FeedRepository.list diff --git a/spec/commands/feeds/add_new_feed_spec.rb b/spec/commands/feeds/add_new_feed_spec.rb index 52545fab2..2c0c77d93 100644 --- a/spec/commands/feeds/add_new_feed_spec.rb +++ b/spec/commands/feeds/add_new_feed_spec.rb @@ -45,5 +45,16 @@ end end end + + it "uses feed_url as name when title is not present" do + feed_url = "https://protomen.com/news/feed" + result = instance_double(Feedjira::Parser::RSS, title: nil, feed_url:) + discoverer = instance_double(FeedDiscovery, discover: result) + + expect { AddNewFeed.add(feed_url, discoverer) } + .to change(Feed, :count).by(1) + + expect(Feed.last.name).to eq(feed_url) + end end end From 147db851d4f276c8f2dffe147cf6a535def21df5 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 29 Dec 2022 14:59:15 -0800 Subject: [PATCH 0441/1107] RuboCop: disable StringHashKeys cop (#747) As much as I might prefer consistency, this one seems hard to enforce safely. --- .rubocop.yml | 1 + .rubocop_todo.yml | 20 -------------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 84149d247..52d907f21 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -44,3 +44,4 @@ Style/InlineComment: { Enabled: false } Style/MissingElse: { Enabled: false } Style/RequireOrder: { Enabled: false } Style/SafeNavigation: { Enabled: false } +Style/StringHashKeys: { Enabled: false } diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index fc8c0b673..1561a2104 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -998,26 +998,6 @@ Style/StaticClass: - 'app/utils/api_key.rb' - 'app/utils/content_sanitizer.rb' -# Offense count: 19 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/StringHashKeys: - Exclude: - - 'app/helpers/controller_helpers.rb' - - 'fever_api.rb' - - 'spec/app_spec.rb' - - 'spec/controllers/debug_controller_spec.rb' - - 'spec/controllers/first_run_controller_spec.rb' - - 'spec/controllers/imports_controller_spec.rb' - - 'spec/controllers/sessions_controller_spec.rb' - - 'spec/fever_api/read_favicons_spec.rb' - - 'spec/fever_api/read_feeds_groups_spec.rb' - - 'spec/fever_api/read_feeds_spec.rb' - - 'spec/fever_api/read_groups_spec.rb' - - 'spec/fever_api/read_items_spec.rb' - - 'spec/fever_api/read_links_spec.rb' - - 'spec/fever_api/sync_saved_item_ids_spec.rb' - - 'spec/fever_api/sync_unread_item_ids_spec.rb' - # Offense count: 7 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinSize. From 9a249e18bfc437649bf8aa77d8fa27e7285f3849 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 29 Dec 2022 15:26:44 -0800 Subject: [PATCH 0442/1107] RuboCop: inspect blocks for RedundantLineBreak (#748) --- .rubocop.yml | 1 + app/commands/feeds/export_to_opml.rb | 4 +--- app/fever_api/write_mark_item.rb | 4 +--- app/utils/feed_discovery.rb | 4 +--- spec/commands/find_new_stories_spec.rb | 4 +--- spec/factories/groups.rb | 4 +--- spec/fever_api/read_feeds_groups_spec.rb | 4 +--- spec/fever_api/read_feeds_spec.rb | 8 ++------ spec/fever_api/read_items_spec.rb | 4 +--- spec/fever_api/write_mark_feed_spec.rb | 4 +--- spec/fever_api/write_mark_group_spec.rb | 4 +--- spec/fever_api/write_mark_item_spec.rb | 4 +--- spec/fever_api_spec.rb | 4 +--- spec/integration/feed_importing_spec.rb | 8 ++------ spec/javascript/test_controller.rb | 8 ++------ spec/repositories/story_repository_spec.rb | 4 +--- 16 files changed, 19 insertions(+), 54 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 52d907f21..0dbf283c5 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,6 +12,7 @@ AllCops: - 'vendor/**/*' Layout/LineLength: { Max: 80 } +Layout/RedundantLineBreak: { InspectBlocks: true } Metrics/BlockLength: { Exclude: ['spec/**/*_spec.rb'] } Style/MethodCallWithArgsParentheses: AllowedMethods: diff --git a/app/commands/feeds/export_to_opml.rb b/app/commands/feeds/export_to_opml.rb index c6e37810c..a8e6e457e 100644 --- a/app/commands/feeds/export_to_opml.rb +++ b/app/commands/feeds/export_to_opml.rb @@ -9,9 +9,7 @@ def to_xml builder = Nokogiri::XML::Builder.new do |xml| xml.opml(version: "1.0") do - xml.head do - xml.title "Feeds from Stringer" - end + xml.head { xml.title "Feeds from Stringer" } xml.body do @feeds.each do |feed| xml.outline( diff --git a/app/fever_api/write_mark_item.rb b/app/fever_api/write_mark_item.rb index fe18a78b4..7d35e31db 100644 --- a/app/fever_api/write_mark_item.rb +++ b/app/fever_api/write_mark_item.rb @@ -8,9 +8,7 @@ class WriteMarkItem def initialize(options = {}) @read_marker_class = options.fetch(:read_marker_class) { MarkAsRead } @unread_marker_class = - options.fetch(:unread_marker_class) do - MarkAsUnread - end + options.fetch(:unread_marker_class) { MarkAsUnread } @starred_marker_class = options.fetch(:starred_marker_class) { MarkAsStarred } @unstarred_marker_class = diff --git a/app/utils/feed_discovery.rb b/app/utils/feed_discovery.rb index c55ad3484..83f4d5a53 100644 --- a/app/utils/feed_discovery.rb +++ b/app/utils/feed_discovery.rb @@ -8,9 +8,7 @@ def discover(url, finder = Feedbag, parser = Feedjira, client = HTTParty) urls = finder.find(url) return false if urls.empty? - get_feed_for_url(urls.first, parser, client) do - return false - end + get_feed_for_url(urls.first, parser, client) { return false } end end diff --git a/spec/commands/find_new_stories_spec.rb b/spec/commands/find_new_stories_spec.rb index c0992e592..c6d542a30 100644 --- a/spec/commands/find_new_stories_spec.rb +++ b/spec/commands/find_new_stories_spec.rb @@ -6,9 +6,7 @@ describe FindNewStories do describe "#new_stories" do context "the feed contains no new stories" do - before do - allow(StoryRepository).to receive(:exists?).and_return(true) - end + before { allow(StoryRepository).to receive(:exists?).and_return(true) } it "should find zero new stories" do story1 = double(published: nil, id: "story1") diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index 3feda95ef..21306b6bb 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -1,3 +1 @@ -FactoryBot.define do - factory(:group) -end +FactoryBot.define { factory(:group) } diff --git a/spec/fever_api/read_feeds_groups_spec.rb b/spec/fever_api/read_feeds_groups_spec.rb index 81204fc3a..6540503dd 100644 --- a/spec/fever_api/read_feeds_groups_spec.rb +++ b/spec/fever_api/read_feeds_groups_spec.rb @@ -7,9 +7,7 @@ let(:feeds) { feed_ids.map { |id| double("feed", id: id, group_id: 1) } } let(:feed_repository) { double("repo") } - subject do - FeverAPI::ReadFeedsGroups.new(feed_repository: feed_repository) - end + subject { FeverAPI::ReadFeedsGroups.new(feed_repository: feed_repository) } it "returns a list of groups requested through feeds" do allow(feed_repository) diff --git a/spec/fever_api/read_feeds_spec.rb b/spec/fever_api/read_feeds_spec.rb index 3b0c36998..5ab15483c 100644 --- a/spec/fever_api/read_feeds_spec.rb +++ b/spec/fever_api/read_feeds_spec.rb @@ -5,15 +5,11 @@ describe FeverAPI::ReadFeeds do let(:feed_ids) { [5, 7, 11] } let(:feeds) do - feed_ids.map do |id| - double("feed", id: id, as_fever_json: { id: id }) - end + feed_ids.map { |id| double("feed", id: id, as_fever_json: { id: id }) } end let(:feed_repository) { double("repo") } - subject do - FeverAPI::ReadFeeds.new(feed_repository: feed_repository) - end + subject { FeverAPI::ReadFeeds.new(feed_repository: feed_repository) } it "returns a list of feeds" do expect(feed_repository).to receive(:list).and_return(feeds) diff --git a/spec/fever_api/read_items_spec.rb b/spec/fever_api/read_items_spec.rb index b56eca152..d146af044 100644 --- a/spec/fever_api/read_items_spec.rb +++ b/spec/fever_api/read_items_spec.rb @@ -5,9 +5,7 @@ describe FeverAPI::ReadItems do let(:story_repository) { double("repo") } - subject do - FeverAPI::ReadItems.new(story_repository: story_repository) - end + subject { FeverAPI::ReadItems.new(story_repository: story_repository) } it "returns a list of unread items including total count" do stories = [ diff --git a/spec/fever_api/write_mark_feed_spec.rb b/spec/fever_api/write_mark_feed_spec.rb index 65efde1d8..719b9ed65 100644 --- a/spec/fever_api/write_mark_feed_spec.rb +++ b/spec/fever_api/write_mark_feed_spec.rb @@ -6,9 +6,7 @@ let(:feed_marker) { double("feed marker") } let(:marker_class) { double("marker class") } - subject do - FeverAPI::WriteMarkFeed.new(marker_class: marker_class) - end + subject { FeverAPI::WriteMarkFeed.new(marker_class: marker_class) } it "instantiates a feed marker and calls mark_feed_as_read if requested" do expect(marker_class) diff --git a/spec/fever_api/write_mark_group_spec.rb b/spec/fever_api/write_mark_group_spec.rb index 91044ea56..9eaa6ee6a 100644 --- a/spec/fever_api/write_mark_group_spec.rb +++ b/spec/fever_api/write_mark_group_spec.rb @@ -6,9 +6,7 @@ let(:group_marker) { double("group marker") } let(:marker_class) { double("marker class") } - subject do - FeverAPI::WriteMarkGroup.new(marker_class: marker_class) - end + subject { FeverAPI::WriteMarkGroup.new(marker_class: marker_class) } it "instantiates a group marker and calls mark_group_as_read if requested" do expect(marker_class) diff --git a/spec/fever_api/write_mark_item_spec.rb b/spec/fever_api/write_mark_item_spec.rb index 483d2eba9..9efd634cc 100644 --- a/spec/fever_api/write_mark_item_spec.rb +++ b/spec/fever_api/write_mark_item_spec.rb @@ -7,9 +7,7 @@ let(:marker_class) { double("marker class") } describe "as read" do - subject do - FeverAPI::WriteMarkItem.new(read_marker_class: marker_class) - end + subject { FeverAPI::WriteMarkItem.new(read_marker_class: marker_class) } it "calls mark_item_as_read if requested" do expect(marker_class).to receive(:new).with(5).and_return(item_marker) diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index 914f4bc5e..9f656ed3e 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -17,9 +17,7 @@ def app let(:standard_answer) do { api_version: 3, auth: 1, last_refreshed_on_time: 123456789 } end - let(:cannot_auth) do - { api_version: 3, auth: 0 } - end + let(:cannot_auth) { { api_version: 3, auth: 0 } } let(:headers) { { api_key: api_key } } before do diff --git a/spec/integration/feed_importing_spec.rb b/spec/integration/feed_importing_spec.rb index f7d99df80..200e0bb3d 100644 --- a/spec/integration/feed_importing_spec.rb +++ b/spec/integration/feed_importing_spec.rb @@ -7,9 +7,7 @@ app_require "tasks/fetch_feed" describe "Feed importing" do - before(:all) do - @server = FeedServer.new - end + before(:all) { @server = FeedServer.new } let(:feed) do Feed.create( @@ -57,9 +55,7 @@ end describe "Feed with incorrect pubdates" do - before(:all) do - Timecop.freeze Time.parse("2014-08-12T17:30:00Z") - end + before(:all) { Timecop.freeze Time.parse("2014-08-12T17:30:00Z") } context "has been fetched before" do it "imports all new stories" do diff --git a/spec/javascript/test_controller.rb b/spec/javascript/test_controller.rb index cb6025913..84a5f6b59 100644 --- a/spec/javascript/test_controller.rb +++ b/spec/javascript/test_controller.rb @@ -36,15 +36,11 @@ def vendor_js_files end def vendor_css_files - ["mocha.css"].map do |name| - File.join "vendor", "css", name - end + ["mocha.css"].map { |name| File.join "vendor", "css", name } end def js_helper_files - ["spec_helper.js"].map do |name| - File.join "spec", name - end + ["spec_helper.js"].map { |name| File.join "spec", name } end def js_lib_files diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 816bedb6a..bc8f96949 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -6,9 +6,7 @@ describe StoryRepository do describe ".add" do let(:feed) { double(url: "http://blog.golang.org/feed.atom") } - before do - allow(Story).to receive(:create) - end + before { allow(Story).to receive(:create) } it "normalizes story urls" do entry = double( From c056f3f017c8dcddd32f747596a43e447cf9b621 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 29 Dec 2022 15:39:29 -0800 Subject: [PATCH 0443/1107] RuboCop: refactor FeedsController#create (#749) --- .rubocop_todo.yml | 6 ------ app/controllers/feeds_controller.rb | 17 ++++++++--------- config/locales/de.yml | 8 ++++---- config/locales/el-GR.yml | 8 ++++---- config/locales/en.yml | 8 ++++---- config/locales/eo.yml | 8 ++++---- config/locales/es.yml | 8 ++++---- config/locales/fr.yml | 8 ++++---- config/locales/he.yml | 8 ++++---- config/locales/it.yml | 8 ++++---- config/locales/ja.yml | 8 ++++---- config/locales/nl.yml | 8 ++++---- config/locales/pt-BR.yml | 8 ++++---- config/locales/pt.yml | 8 ++++---- config/locales/ru.yml | 8 ++++---- config/locales/sv.yml | 8 ++++---- config/locales/tr.yml | 8 ++++---- config/locales/zh-CN.yml | 8 ++++---- config/locales/zh-TW.yml | 8 ++++---- 19 files changed, 76 insertions(+), 83 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 1561a2104..f4cfe9195 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -24,12 +24,6 @@ Lint/NumberConversion: - 'spec/models/feed_spec.rb' - 'spec/models/story_spec.rb' -# Offense count: 1 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, CountRepeatedAttributes, Max. -Metrics/AbcSize: - Exclude: - - 'app/controllers/feeds_controller.rb' - # Offense count: 15 # Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods. Metrics/MethodLength: diff --git a/app/controllers/feeds_controller.rb b/app/controllers/feeds_controller.rb index 298cea139..0073914a4 100644 --- a/app/controllers/feeds_controller.rb +++ b/app/controllers/feeds_controller.rb @@ -21,18 +21,17 @@ def create @feed_url = params[:feed_url] feed = AddNewFeed.add(@feed_url) - if feed && feed.valid? - FetchFeeds.enqueue([feed]) + unless feed && feed.valid? + flash.now[:error] = feed ? t(".already_subscribed") : t(".feed_not_found") - flash[:success] = t("feeds.add.flash.added_successfully") - redirect_to("/") - elsif feed - flash.now[:error] = t("feeds.add.flash.already_subscribed_error") - render(:new) - else - flash.now[:error] = t("feeds.add.flash.feed_not_found_error") render(:new) + return end + + FetchFeeds.enqueue([feed]) + + flash[:success] = t(".success") + redirect_to("/") end def update diff --git a/config/locales/de.yml b/config/locales/de.yml index bef897ecb..7eb5b516c 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -25,11 +25,11 @@ de: fields: feed_url: Feed URL submit: Hinzufügen - flash: - added_successfully: Wir haben deinen Feed hinzugefügt. Schau ein bisschen später wieder vorbei. - already_subscribed_error: Du hast diesen Feed bereits abonniert... - feed_not_found_error: Wir konnten diesen Feed nicht finden. Probiere es noch einmal title: Benötigst du neue Geschichten? + create: + success: Wir haben deinen Feed hinzugefügt. Schau ein bisschen später wieder vorbei. + already_subscribed: Du hast diesen Feed bereits abonniert... + feed_not_found: Wir konnten diesen Feed nicht finden. Probiere es noch einmal edit: fields: feed_name: Feed-Name diff --git a/config/locales/el-GR.yml b/config/locales/el-GR.yml index 79f70e364..5e7851c4a 100644 --- a/config/locales/el-GR.yml +++ b/config/locales/el-GR.yml @@ -25,11 +25,11 @@ el-GR: fields: feed_url: Διεύθυνση submit: Προσθήκη - flash: - added_successfully: Το καινούριο σας Ιστολόγιο προστέθηκε. Ελέγξτε παλι αργότερα. - already_subscribed_error: Είστε ήδη εγγεγραμενος σ' αυτο το ιστολόγιο... - feed_not_found_error: Δεν μπορέσαμε να βρούμε αυτο το ιστολόγιο. Προσπαθήστε ξανά. title: Όρεξη για καινούριες ειδήσεις? + create: + success: Το καινούριο σας Ιστολόγιο προστέθηκε. Ελέγξτε παλι αργότερα. + already_subscribed: Είστε ήδη εγγεγραμενος σ' αυτο το ιστολόγιο... + feed_not_found: Δεν μπορέσαμε να βρούμε αυτο το ιστολόγιο. Προσπαθήστε ξανά. edit: fields: feed_name: diff --git a/config/locales/en.yml b/config/locales/en.yml index 8eb38af8d..1fe5263c7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -25,11 +25,11 @@ en: fields: feed_url: Feed URL submit: Add - flash: - added_successfully: We've added your new feed. Check back in a bit. - already_subscribed_error: You are already subscribed to this feed... - feed_not_found_error: We couldn't find that feed. Try again. title: Need new stories? + create: + success: We've added your new feed. Check back in a bit. + already_subscribed: You are already subscribed to this feed... + feed_not_found: We couldn't find that feed. Try again. edit: fields: feed_name: Feed Name diff --git a/config/locales/eo.yml b/config/locales/eo.yml index 4dd73b787..a779bff14 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -25,11 +25,11 @@ eo: fields: feed_url: URL de fluo submit: Aldoni - flash: - added_successfully: Ni aldonis vian nova fluon. Revenu pli poste. - already_subscribed_error: Vi abonis tiun fluon jam... - feed_not_found_error: Ni ne eblas trovi tion fluon. Reprovu. title: Ĉu vi bezonas novajn rakontojn? + create: + success: Ni aldonis vian nova fluon. Revenu pli poste. + already_subscribed: Vi abonis tiun fluon jam... + feed_not_found: Ni ne eblas trovi tion fluon. Reprovu. edit: fields: feed_name: Nomo de fluo diff --git a/config/locales/es.yml b/config/locales/es.yml index 6f7a21446..353c8dae8 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -25,11 +25,11 @@ es: fields: feed_url: URL de la feed submit: Añadir - flash: - added_successfully: Hemos agregado tu nueva feed. Regresa en un ratito. - already_subscribed_error: Ya te suscribiste a esta feed... - feed_not_found_error: No pudimos encontrar esa feed. Inténtalo de vuelta. title: ¿Necesitas nuevas historias? + create: + success: Hemos agregado tu nueva feed. Regresa en un ratito. + already_subscribed: Ya te suscribiste a esta feed... + feed_not_found: No pudimos encontrar esa feed. Inténtalo de vuelta. edit: fields: feed_name: Nombre fuente diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 172e6328a..fd927a3a6 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -25,11 +25,11 @@ fr: fields: feed_url: URL du flux submit: Ajouter - flash: - added_successfully: Le nouveau flux a été ajouté. Patientez quelques instants. - already_subscribed_error: Vous suivez déjà ce flux... - feed_not_found_error: Nous n'avons pas pu trouver ce flux. Essayez de nouveau. title: Besoin de nouveaux articles ? + create: + success: Le nouveau flux a été ajouté. Patientez quelques instants. + already_subscribed: Vous suivez déjà ce flux... + feed_not_found: Nous n'avons pas pu trouver ce flux. Essayez de nouveau. edit: fields: feed_name: diff --git a/config/locales/he.yml b/config/locales/he.yml index 9e6d2c96d..7bb881c9f 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -25,11 +25,11 @@ he: fields: feed_url: כתובת של הפיד submit: להוסיף - flash: - added_successfully: אנחנו הוספנו את הפיד החדש. עוד מעט תקבל עידכונים. - already_subscribed_error: הפיד הנ"ל כבר נמצא במעקב. - feed_not_found_error: לא הצלחנו למצוא את הפיד. נסה שוב. title: מחפש מה לקרוא? + create: + success: אנחנו הוספנו את הפיד החדש. עוד מעט תקבל עידכונים. + already_subscribed: הפיד הנ"ל כבר נמצא במעקב. + feed_not_found: לא הצלחנו למצוא את הפיד. נסה שוב. edit: fields: feed_name: diff --git a/config/locales/it.yml b/config/locales/it.yml index d0b4cbaea..0e4277039 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -25,11 +25,11 @@ it: fields: feed_url: URL del feed submit: Aggiungi - flash: - added_successfully: Abbiamo aggiunto il tuo nuovo feed. Ripassa tra qualche istante. - already_subscribed_error: Sei già sottoscritto a questo feed... - feed_not_found_error: Non siamo riusciti a trovare il feed. Riprova. title: Bisogno di nuove storie? + create: + success: Abbiamo aggiunto il tuo nuovo feed. Ripassa tra qualche istante. + already_subscribed: Sei già sottoscritto a questo feed... + feed_not_found: Non siamo riusciti a trovare il feed. Riprova. edit: fields: feed_name: diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 4f30eb756..9597207a2 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -25,11 +25,11 @@ ja: fields: feed_url: フィードURL submit: 追加 - flash: - added_successfully: 新しいフィードを追加しました、少し経ってから確認して下さい - already_subscribed_error: このフィードは既に登録されてます - feed_not_found_error: フィードを見つけられませんでした、もう一度試して下さい title: 新しいストーリーが必要ですか? + create: + success: 新しいフィードを追加しました、少し経ってから確認して下さい + already_subscribed: このフィードは既に登録されてます + feed_not_found: フィードを見つけられませんでした、もう一度試して下さい edit: fields: feed_name: フィード名 diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 115a908e4..dc32a0f62 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -25,11 +25,11 @@ nl: fields: feed_url: Feed-URL submit: Toevoegen - flash: - added_successfully: We hebben je nieuwe feed toegevoegd. Kijk over een tijdje nog eens. - already_subscribed_error: Je bent al geabonneerd op deze feed... - feed_not_found_error: Die feed konden we niet vinden. Probeer het opnieuw. title: Nieuwe artikelen nodig? + create: + success: We hebben je nieuwe feed toegevoegd. Kijk over een tijdje nog eens. + already_subscribed: Je bent al geabonneerd op deze feed... + feed_not_found: Die feed konden we niet vinden. Probeer het opnieuw. edit: fields: feed_name: Feednaam diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 0a18bfe00..7942b68e4 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -25,11 +25,11 @@ pt-BR: fields: feed_url: URL do Feed submit: Adicionar - flash: - added_successfully: Nós estamos adicionando um novo feed. Volte daqui a pouco. - already_subscribed_error: Você já está inscrito neste feed... - feed_not_found_error: Não conseguimos achar este feed. Tente novamente. title: Precisa de novas histórias? + create: + success: Nós estamos adicionando um novo feed. Volte daqui a pouco. + already_subscribed: Você já está inscrito neste feed... + feed_not_found: Não conseguimos achar este feed. Tente novamente. edit: fields: feed_name: diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 054b19bb8..45a5424d2 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -25,11 +25,11 @@ pt: fields: feed_url: URL da Feed submit: Adicionar - flash: - added_successfully: Adicionamos a sua nova feed. Verifique novamente mais tarde. - already_subscribed_error: Você já subscreveu esta feed... - feed_not_found_error: Não foi possível encontrar a feed. Tente novamente. title: Precisa de novas histórias? + create: + success: Adicionamos a sua nova feed. Verifique novamente mais tarde. + already_subscribed: Você já subscreveu esta feed... + feed_not_found: Não foi possível encontrar a feed. Tente novamente. edit: fields: feed_name: diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 30299b1ab..a111fcf8e 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -25,11 +25,11 @@ ru: fields: feed_url: URL фида submit: Добавить - flash: - added_successfully: Мы добавили новый фид. Скоро информация обновится. - already_subscribed_error: Вы уже подписаны на этот фид... - feed_not_found_error: Мы не смогли найти этот фид. Попробуйте еще раз. title: Нужны новые истории? + create: + success: Мы добавили новый фид. Скоро информация обновится. + already_subscribed: Вы уже подписаны на этот фид... + feed_not_found: Мы не смогли найти этот фид. Попробуйте еще раз. edit: fields: feed_name: diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 12487e118..0067e9205 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -25,11 +25,11 @@ sv: fields: feed_url: Feedens URL submit: Lägg till - flash: - added_successfully: Vi har lagt till din nya feed. Kom tillbaks om en stund. - already_subscribed_error: Du prenumererar redan på den här feeden... - feed_not_found_error: Vi kunde inte hitta feeden. Prova igen. title: Behöver du nya berättelser? + create: + success: Vi har lagt till din nya feed. Kom tillbaks om en stund. + already_subscribed: Du prenumererar redan på den här feeden... + feed_not_found: Vi kunde inte hitta feeden. Prova igen. edit: fields: feed_name: Feednamn diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 26523682f..27ce5ecb9 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -25,11 +25,11 @@ tr: fields: feed_url: Besleme URL'si submit: Ekle - flash: - added_successfully: Yeni besleme eklenmistir. Sonra kontrol edin. - already_subscribed_error: Bu beslemeye zaten kayitlisiniz... - feed_not_found_error: Bu beslemeyi bulamadik. Tekrar deneyiniz. title: Yeni hikayelere mi ihtiyaciniz var? + create: + success: Yeni besleme eklenmistir. Sonra kontrol edin. + already_subscribed: Bu beslemeye zaten kayitlisiniz... + feed_not_found: Bu beslemeyi bulamadik. Tekrar deneyiniz. edit: fields: feed_name: diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 275dee1ad..4fd5432f3 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -25,11 +25,11 @@ zh-CN: fields: feed_url: 供稿地址 submit: 添加 - flash: - added_successfully: 你的订阅已经添加完毕,稍等一段时间就可以阅读啦 - already_subscribed_error: 你已经订阅过这个供稿了哟... - feed_not_found_error: 呃,我们无法识别这个供稿地址。麻烦你检查后再试一次。 title: 想要添加新内容? + create: + success: 你的订阅已经添加完毕,稍等一段时间就可以阅读啦 + already_subscribed: 你已经订阅过这个供稿了哟... + feed_not_found: 呃,我们无法识别这个供稿地址。麻烦你检查后再试一次。 edit: fields: feed_name: diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 9eef85581..1cba21e32 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -25,11 +25,11 @@ zh-TW: fields: feed_url: RSS 訊息來源 submit: 新增 - flash: - added_successfully: 你的訂閱已經新增完畢,請稍等一段時間再回來查看 - already_subscribed_error: 你已經訂閱過這個訊息來源了 - feed_not_found_error: 我們找不到這個訊息來源,麻煩檢查並稍後後再試一次 title: 想要新增新內容? + create: + success: 你的訂閱已經新增完畢,請稍等一段時間再回來查看 + already_subscribed: 你已經訂閱過這個訊息來源了 + feed_not_found: 我們找不到這個訊息來源,麻煩檢查並稍後後再試一次 edit: fields: feed_name: 訊息來源名稱 From 86a8f098c1665c1f687b476ee711194f80fabc3f Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 29 Dec 2022 15:54:33 -0800 Subject: [PATCH 0444/1107] RuboCop: configure a handful of rules (#750) --- .rubocop.yml | 8 ++- .rubocop_todo.yml | 139 ++++++---------------------------------------- 2 files changed, 24 insertions(+), 123 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 0dbf283c5..fe813b383 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -14,18 +14,20 @@ AllCops: Layout/LineLength: { Max: 80 } Layout/RedundantLineBreak: { InspectBlocks: true } Metrics/BlockLength: { Exclude: ['spec/**/*_spec.rb'] } +RSpec/MessageExpectation: { EnforcedStyle: expect } +RSpec/MessageSpies: { EnforcedStyle: receive } Style/MethodCallWithArgsParentheses: AllowedMethods: - to - not_to - describe Style/StringLiterals: { EnforcedStyle: double_quotes } +Style/SymbolArray: { EnforcedStyle: brackets } +Style/WordArray: { EnforcedStyle: brackets } # want to enable these, but they don't work right when using `.rubocop_todo.yml` Style/DocumentationMethod: { Enabled: false } Style/Documentation: { Enabled: false } -Style/SymbolArray: { EnforcedStyle: brackets } -Style/WordArray: { EnforcedStyle: brackets } ################################################################################ # @@ -37,6 +39,8 @@ Bundler/GemComment: { Enabled: false } Bundler/GemVersion: { Enabled: false } Layout/SingleLineBlockChain: { Enabled: false } Lint/ConstantResolution: { Enabled: false } +RSpec/AlignLeftLetBrace: { Enabled: false } +RSpec/AlignRightLetBrace: { Enabled: false } RSpec/StubbedMock: { Enabled: false } Rails/SchemaComment: { Enabled: false } Style/ConstantVisibility: { Enabled: false } diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f4cfe9195..24a4969be 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,12 +1,12 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 400` -# on 2022-12-19 07:00:21 UTC using RuboCop version 1.40.0. +# on 2022-12-20 23:41:09 UTC using RuboCop version 1.40.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 12 +# Offense count: 10 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, IgnoredClasses. # AllowedMethods: ago, from_now, second, seconds, minute, minutes, hour, hours, day, days, week, weeks, fortnight, fortnights, in_milliseconds @@ -24,12 +24,11 @@ Lint/NumberConversion: - 'spec/models/feed_spec.rb' - 'spec/models/story_spec.rb' -# Offense count: 15 +# Offense count: 12 # Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods. Metrics/MethodLength: Exclude: - 'app/commands/feeds/export_to_opml.rb' - - 'app/controllers/feeds_controller.rb' - 'app/fever_api/read_items.rb' - 'app/helpers/url_helpers.rb' - 'app/models/story.rb' @@ -59,52 +58,6 @@ Naming/PredicateName: Exclude: - 'app/utils/sample_story.rb' -# Offense count: 31 -# This cop supports safe autocorrection (--autocorrect). -RSpec/AlignLeftLetBrace: - Exclude: - - 'spec/commands/feeds/add_new_feed_spec.rb' - - 'spec/commands/feeds/export_to_opml_spec.rb' - - 'spec/commands/stories/mark_all_as_read_spec.rb' - - 'spec/commands/stories/mark_feed_as_read_spec.rb' - - 'spec/commands/stories/mark_group_as_read_spec.rb' - - 'spec/commands/users/sign_in_user_spec.rb' - - 'spec/controllers/feeds_controller_spec.rb' - - 'spec/controllers/first_run_controller_spec.rb' - - 'spec/controllers/stories_controller_spec.rb' - - 'spec/fever_api/read_feeds_groups_spec.rb' - - 'spec/fever_api/sync_saved_item_ids_spec.rb' - - 'spec/fever_api/sync_unread_item_ids_spec.rb' - - 'spec/fever_api/write_mark_feed_spec.rb' - - 'spec/fever_api/write_mark_item_spec.rb' - - 'spec/fever_api_spec.rb' - - 'spec/tasks/fetch_feed_spec.rb' - - 'spec/tasks/fetch_feeds_spec.rb' - - 'spec/utils/feed_discovery_spec.rb' - -# Offense count: 35 -# This cop supports safe autocorrection (--autocorrect). -RSpec/AlignRightLetBrace: - Exclude: - - 'spec/commands/feeds/add_new_feed_spec.rb' - - 'spec/commands/stories/mark_all_as_read_spec.rb' - - 'spec/commands/stories/mark_feed_as_read_spec.rb' - - 'spec/commands/stories/mark_group_as_read_spec.rb' - - 'spec/commands/users/change_user_password_spec.rb' - - 'spec/commands/users/sign_in_user_spec.rb' - - 'spec/controllers/feeds_controller_spec.rb' - - 'spec/controllers/first_run_controller_spec.rb' - - 'spec/controllers/stories_controller_spec.rb' - - 'spec/fever_api/read_feeds_groups_spec.rb' - - 'spec/fever_api/sync_saved_item_ids_spec.rb' - - 'spec/fever_api/sync_unread_item_ids_spec.rb' - - 'spec/fever_api/write_mark_feed_spec.rb' - - 'spec/fever_api/write_mark_item_spec.rb' - - 'spec/fever_api_spec.rb' - - 'spec/tasks/fetch_feed_spec.rb' - - 'spec/tasks/fetch_feeds_spec.rb' - - 'spec/utils/feed_discovery_spec.rb' - # Offense count: 5 RSpec/AnyInstance: Exclude: @@ -286,12 +239,6 @@ RSpec/LeadingSubject: - 'spec/fever_api/write_mark_feed_spec.rb' - 'spec/fever_api/write_mark_group_spec.rb' -# Offense count: 2 -RSpec/LeakyConstantDeclaration: - Exclude: - - 'spec/helpers/authentications_helper_spec.rb' - - 'spec/helpers/url_helpers_spec.rb' - # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). RSpec/LetBeforeExamples: @@ -304,75 +251,34 @@ RSpec/MessageChain: - 'spec/fever_api/read_feeds_groups_spec.rb' - 'spec/fever_api_spec.rb' -# Offense count: 106 +# Offense count: 53 # Configuration parameters: EnforcedStyle. # SupportedStyles: allow, expect RSpec/MessageExpectation: Exclude: - 'spec/commands/feeds/add_new_feed_spec.rb' - - 'spec/commands/stories/mark_all_as_read_spec.rb' - - 'spec/commands/stories/mark_feed_as_read_spec.rb' - - 'spec/commands/stories/mark_group_as_read_spec.rb' - - 'spec/commands/users/change_user_password_spec.rb' - - 'spec/commands/users/complete_setup_spec.rb' - - 'spec/commands/users/create_user_spec.rb' - - 'spec/controllers/feeds_controller_spec.rb' + - 'spec/commands/find_new_stories_spec.rb' + - 'spec/controllers/debug_controller_spec.rb' + - 'spec/controllers/exports_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' - - 'spec/controllers/imports_controller_spec.rb' + - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - - 'spec/fever_api/authentication_spec.rb' - - 'spec/fever_api/read_feeds_spec.rb' - - 'spec/fever_api/read_groups_spec.rb' - - 'spec/fever_api/read_items_spec.rb' - - 'spec/fever_api/sync_saved_item_ids_spec.rb' - - 'spec/fever_api/sync_unread_item_ids_spec.rb' - - 'spec/fever_api/write_mark_feed_spec.rb' - - 'spec/fever_api/write_mark_group_spec.rb' - - 'spec/fever_api/write_mark_item_spec.rb' - 'spec/fever_api_spec.rb' - - 'spec/jobs/fetch_feed_job_spec.rb' + - 'spec/helpers/authentications_helper_spec.rb' + - 'spec/models/migration_status_spec.rb' - 'spec/repositories/feed_repository_spec.rb' - 'spec/repositories/story_repository_spec.rb' - - 'spec/tasks/change_password_spec.rb' - 'spec/tasks/fetch_feed_spec.rb' - 'spec/tasks/fetch_feeds_spec.rb' - 'spec/tasks/remove_old_stories_spec.rb' - - 'spec/utils/feed_discovery_spec.rb' + - 'spec/utils/i18n_support_spec.rb' -# Offense count: 111 +# Offense count: 1 # Configuration parameters: EnforcedStyle. # SupportedStyles: have_received, receive RSpec/MessageSpies: Exclude: - 'spec/commands/feeds/add_new_feed_spec.rb' - - 'spec/commands/stories/mark_all_as_read_spec.rb' - - 'spec/commands/stories/mark_feed_as_read_spec.rb' - - 'spec/commands/stories/mark_group_as_read_spec.rb' - - 'spec/commands/users/change_user_password_spec.rb' - - 'spec/commands/users/complete_setup_spec.rb' - - 'spec/commands/users/create_user_spec.rb' - - 'spec/controllers/feeds_controller_spec.rb' - - 'spec/controllers/first_run_controller_spec.rb' - - 'spec/controllers/imports_controller_spec.rb' - - 'spec/controllers/stories_controller_spec.rb' - - 'spec/fever_api/authentication_spec.rb' - - 'spec/fever_api/read_feeds_spec.rb' - - 'spec/fever_api/read_groups_spec.rb' - - 'spec/fever_api/read_items_spec.rb' - - 'spec/fever_api/sync_saved_item_ids_spec.rb' - - 'spec/fever_api/sync_unread_item_ids_spec.rb' - - 'spec/fever_api/write_mark_feed_spec.rb' - - 'spec/fever_api/write_mark_group_spec.rb' - - 'spec/fever_api/write_mark_item_spec.rb' - - 'spec/fever_api_spec.rb' - - 'spec/jobs/fetch_feed_job_spec.rb' - - 'spec/repositories/feed_repository_spec.rb' - - 'spec/repositories/story_repository_spec.rb' - - 'spec/tasks/change_password_spec.rb' - - 'spec/tasks/fetch_feed_spec.rb' - - 'spec/tasks/fetch_feeds_spec.rb' - - 'spec/tasks/remove_old_stories_spec.rb' - - 'spec/utils/feed_discovery_spec.rb' # Offense count: 96 # Configuration parameters: Max. @@ -607,7 +513,7 @@ Rails/SkipsModelValidations: - 'db/migrate/20140421224454_fix_invalid_unicode.rb' - 'db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb' -# Offense count: 26 +# Offense count: 24 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: strict, flexible @@ -673,7 +579,7 @@ Style/FetchEnvVar: Exclude: - 'Rakefile' -# Offense count: 153 +# Offense count: 149 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never @@ -829,7 +735,7 @@ Style/FrozenStringLiteralComment: - 'spec/utils/i18n_support_spec.rb' - 'spec/utils/opml_parser_spec.rb' -# Offense count: 91 +# Offense count: 86 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys @@ -867,7 +773,7 @@ Style/HashSyntax: - 'spec/tasks/change_password_spec.rb' - 'spec/tasks/fetch_feed_spec.rb' -# Offense count: 185 +# Offense count: 184 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: IgnoreMacros, AllowedMethods, IgnoredMethods, AllowedPatterns, IgnoredPatterns, IncludedMacros, AllowParenthesesInMultilineCall, AllowParenthesesInChaining, AllowParenthesesInCamelCaseMethod, AllowParenthesesInStringInterpolation, EnforcedStyle. # SupportedStyles: require_parentheses, omit_parentheses @@ -936,12 +842,12 @@ Style/NumericLiterals: - 'spec/fever_api/write_mark_group_spec.rb' - 'spec/fever_api_spec.rb' -# Offense count: 6 +# Offense count: 2 Style/OpenStructUse: Exclude: - 'app.rb' -# Offense count: 28 +# Offense count: 21 # Configuration parameters: SuspiciousParamNames, Allowlist. # SuspiciousParamNames: options, opts, args, params, parameters Style/OptionHash: @@ -958,8 +864,6 @@ Style/OptionHash: - 'app/fever_api/write_mark_feed.rb' - 'app/fever_api/write_mark_group.rb' - 'app/fever_api/write_mark_item.rb' - - 'spec/factories/feeds.rb' - - 'spec/factories/groups.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). @@ -992,13 +896,6 @@ Style/StaticClass: - 'app/utils/api_key.rb' - 'app/utils/content_sanitizer.rb' -# Offense count: 7 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: MinSize. -# SupportedStyles: percent, brackets -Style/SymbolArray: - EnforcedStyle: brackets - # Offense count: 6 Style/TopLevelMethodDefinition: Exclude: From f801344068e2f77ff12bb8bd9a19f472f58af943 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 29 Dec 2022 16:01:45 -0800 Subject: [PATCH 0445/1107] RuboCop: enable FrozenStringLiteralComment cop (#751) --- .rubocop_todo.yml | 156 ------------------ Gemfile | 2 + Rakefile | 2 + app.rb | 2 + app/commands/feeds/add_new_feed.rb | 2 + app/commands/feeds/export_to_opml.rb | 2 + app/commands/feeds/find_new_stories.rb | 2 + app/commands/feeds/import_from_opml.rb | 2 + app/commands/stories/mark_all_as_read.rb | 2 + app/commands/stories/mark_as_read.rb | 2 + app/commands/stories/mark_as_starred.rb | 2 + app/commands/stories/mark_as_unread.rb | 2 + app/commands/stories/mark_as_unstarred.rb | 2 + app/commands/stories/mark_feed_as_read.rb | 2 + app/commands/stories/mark_group_as_read.rb | 2 + app/commands/users/change_user_password.rb | 2 + app/commands/users/complete_setup.rb | 2 + app/commands/users/create_user.rb | 2 + app/commands/users/sign_in_user.rb | 2 + app/controllers/debug_controller.rb | 2 + .../sinatra/first_run_controller.rb | 2 + .../sinatra/sessions_controller.rb | 2 + app/controllers/sinatra/stories_controller.rb | 2 + app/fever_api/authentication.rb | 2 + app/fever_api/read_favicons.rb | 4 +- app/fever_api/read_feeds.rb | 2 + app/fever_api/read_feeds_groups.rb | 2 + app/fever_api/read_groups.rb | 2 + app/fever_api/read_items.rb | 2 + app/fever_api/read_links.rb | 2 + app/fever_api/response.rb | 2 + app/fever_api/sync_saved_item_ids.rb | 2 + app/fever_api/sync_unread_item_ids.rb | 2 + app/fever_api/write_mark_feed.rb | 2 + app/fever_api/write_mark_group.rb | 2 + app/fever_api/write_mark_item.rb | 2 + app/helpers/authentication_helpers.rb | 2 + app/helpers/url_helpers.rb | 2 + app/jobs/fetch_feed_job.rb | 2 + app/models/application_record.rb | 2 + app/models/feed.rb | 2 + app/models/group.rb | 2 + app/models/migration_status.rb | 2 + app/models/story.rb | 4 +- app/models/user.rb | 2 + app/repositories/feed_repository.rb | 2 + app/repositories/group_repository.rb | 2 + app/repositories/story_repository.rb | 2 + app/repositories/user_repository.rb | 2 + app/tasks/change_password.rb | 2 + app/tasks/fetch_feed.rb | 2 + app/tasks/fetch_feeds.rb | 2 + app/tasks/remove_old_stories.rb | 2 + app/utils/api_key.rb | 2 + app/utils/content_sanitizer.rb | 2 + app/utils/feed_discovery.rb | 2 + app/utils/opml_parser.rb | 2 + app/utils/sample_story.rb | 4 +- config.ru | 2 + config/asset_pipeline.rb | 2 + config/puma.rb | 2 + db/migrate/20130409010818_create_feeds.rb | 2 + db/migrate/20130409010826_create_stories.rb | 2 + ...0130412185253_add_new_fields_to_stories.rb | 2 + db/migrate/20130418221144_add_user_model.rb | 2 + .../20130423001740_drop_email_from_user.rb | 2 + ...130423180446_remove_author_from_stories.rb | 2 + ...130425211008_add_setup_complete_to_user.rb | 2 + db/migrate/20130425222157_add_delayed_job.rb | 2 + .../20130429232127_add_status_to_feeds.rb | 2 + db/migrate/20130504005816_text_url.rb | 2 + ...504022615_change_story_permalink_column.rb | 2 + .../20130509131045_add_unique_constraints.rb | 2 + ...130513025939_add_keep_unread_to_stories.rb | 2 + ...44029_add_is_starred_status_for_stories.rb | 2 + .../20130522014405_add_api_key_to_user.rb | 2 + .../20130730120312_add_entry_id_to_stories.rb | 2 + ...13712_update_stories_unique_constraints.rb | 2 + .../20130821020313_update_nil_entry_ids.rb | 2 + ...se_text_datatype_for_title_and_entry_id.rb | 2 + ..._groups_table_and_foreign_keys_to_feeds.rb | 2 + .../20140421224454_fix_invalid_unicode.rb | 2 + ...nvalid_titles_with_unicode_line_endings.rb | 2 + fever_api.rb | 2 + spec/app_spec.rb | 2 + spec/commands/feeds/add_new_feed_spec.rb | 2 + spec/commands/feeds/export_to_opml_spec.rb | 2 + spec/commands/feeds/import_from_opml_spec.rb | 2 + spec/commands/find_new_stories_spec.rb | 2 + .../commands/stories/mark_all_as_read_spec.rb | 2 + spec/commands/stories/mark_as_read_spec.rb | 2 + spec/commands/stories/mark_as_starred_spec.rb | 2 + spec/commands/stories/mark_as_unread_spec.rb | 2 + .../stories/mark_as_unstarred_spec.rb | 2 + .../stories/mark_feed_as_read_spec.rb | 2 + .../stories/mark_group_as_read_spec.rb | 2 + .../users/change_user_password_spec.rb | 2 + spec/commands/users/complete_setup_spec.rb | 2 + spec/commands/users/create_user_spec.rb | 2 + spec/commands/users/sign_in_user_spec.rb | 2 + spec/config/asset_pipeline_spec.rb | 2 + spec/controllers/debug_controller_spec.rb | 2 + spec/controllers/exports_controller_spec.rb | 2 + spec/controllers/feeds_controller_spec.rb | 2 + spec/controllers/first_run_controller_spec.rb | 2 + spec/controllers/imports_controller_spec.rb | 2 + spec/controllers/sessions_controller_spec.rb | 2 + spec/controllers/stories_controller_spec.rb | 2 + spec/factories.rb | 2 + spec/factories/feeds.rb | 2 + spec/factories/groups.rb | 2 + spec/factories/stories.rb | 2 + spec/factories/users.rb | 2 + spec/fever_api/authentication_spec.rb | 2 + spec/fever_api/read_favicons_spec.rb | 2 + spec/fever_api/read_feeds_groups_spec.rb | 2 + spec/fever_api/read_feeds_spec.rb | 2 + spec/fever_api/read_groups_spec.rb | 2 + spec/fever_api/read_items_spec.rb | 2 + spec/fever_api/read_links_spec.rb | 2 + spec/fever_api/sync_saved_item_ids_spec.rb | 2 + spec/fever_api/sync_unread_item_ids_spec.rb | 2 + spec/fever_api/write_mark_feed_spec.rb | 2 + spec/fever_api/write_mark_group_spec.rb | 2 + spec/fever_api/write_mark_item_spec.rb | 2 + spec/fever_api_spec.rb | 2 + spec/helpers/authentications_helper_spec.rb | 2 + spec/helpers/url_helpers_spec.rb | 2 + spec/integration/feed_importing_spec.rb | 2 + spec/javascript/test_controller.rb | 2 + spec/models/feed_spec.rb | 2 + spec/models/group_spec.rb | 2 + spec/models/migration_status_spec.rb | 2 + spec/models/story_spec.rb | 2 + spec/repositories/feed_repository_spec.rb | 2 + spec/repositories/group_repository_spec.rb | 2 + spec/repositories/story_repository_spec.rb | 2 + spec/repositories/user_repository_spec.rb | 2 + spec/spec_helper.rb | 2 + spec/support/active_record.rb | 2 + spec/support/coverage.rb | 2 + spec/support/feed_server.rb | 2 + spec/tasks/change_password_spec.rb | 2 + spec/tasks/fetch_feed_spec.rb | 2 + spec/tasks/fetch_feeds_spec.rb | 2 + spec/tasks/remove_old_stories_spec.rb | 2 + spec/utils/content_sanitizer_spec.rb | 2 + spec/utils/feed_discovery_spec.rb | 2 + spec/utils/i18n_support_spec.rb | 2 + spec/utils/opml_parser_spec.rb | 2 + 150 files changed, 301 insertions(+), 159 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 24a4969be..6c1b22b50 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -579,162 +579,6 @@ Style/FetchEnvVar: Exclude: - 'Rakefile' -# Offense count: 149 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: always, always_true, never -Style/FrozenStringLiteralComment: - Exclude: - - 'Gemfile' - - 'Rakefile' - - 'app.rb' - - 'app/commands/feeds/add_new_feed.rb' - - 'app/commands/feeds/export_to_opml.rb' - - 'app/commands/feeds/find_new_stories.rb' - - 'app/commands/feeds/import_from_opml.rb' - - 'app/commands/stories/mark_all_as_read.rb' - - 'app/commands/stories/mark_as_read.rb' - - 'app/commands/stories/mark_as_starred.rb' - - 'app/commands/stories/mark_as_unread.rb' - - 'app/commands/stories/mark_as_unstarred.rb' - - 'app/commands/stories/mark_feed_as_read.rb' - - 'app/commands/stories/mark_group_as_read.rb' - - 'app/commands/users/change_user_password.rb' - - 'app/commands/users/complete_setup.rb' - - 'app/commands/users/create_user.rb' - - 'app/commands/users/sign_in_user.rb' - - 'app/controllers/debug_controller.rb' - - 'app/controllers/sinatra/first_run_controller.rb' - - 'app/controllers/sinatra/sessions_controller.rb' - - 'app/controllers/sinatra/stories_controller.rb' - - 'app/fever_api/authentication.rb' - - 'app/fever_api/read_favicons.rb' - - 'app/fever_api/read_feeds.rb' - - 'app/fever_api/read_feeds_groups.rb' - - 'app/fever_api/read_groups.rb' - - 'app/fever_api/read_items.rb' - - 'app/fever_api/read_links.rb' - - 'app/fever_api/response.rb' - - 'app/fever_api/sync_saved_item_ids.rb' - - 'app/fever_api/sync_unread_item_ids.rb' - - 'app/fever_api/write_mark_feed.rb' - - 'app/fever_api/write_mark_group.rb' - - 'app/fever_api/write_mark_item.rb' - - 'app/helpers/authentication_helpers.rb' - - 'app/helpers/url_helpers.rb' - - 'app/jobs/fetch_feed_job.rb' - - 'app/models/application_record.rb' - - 'app/models/feed.rb' - - 'app/models/group.rb' - - 'app/models/migration_status.rb' - - 'app/models/story.rb' - - 'app/models/user.rb' - - 'app/repositories/feed_repository.rb' - - 'app/repositories/group_repository.rb' - - 'app/repositories/story_repository.rb' - - 'app/repositories/user_repository.rb' - - 'app/tasks/change_password.rb' - - 'app/tasks/fetch_feed.rb' - - 'app/tasks/fetch_feeds.rb' - - 'app/tasks/remove_old_stories.rb' - - 'app/utils/api_key.rb' - - 'app/utils/content_sanitizer.rb' - - 'app/utils/feed_discovery.rb' - - 'app/utils/opml_parser.rb' - - 'app/utils/sample_story.rb' - - 'config.ru' - - 'config/asset_pipeline.rb' - - 'config/puma.rb' - - 'db/migrate/20130409010818_create_feeds.rb' - - 'db/migrate/20130409010826_create_stories.rb' - - 'db/migrate/20130412185253_add_new_fields_to_stories.rb' - - 'db/migrate/20130418221144_add_user_model.rb' - - 'db/migrate/20130423001740_drop_email_from_user.rb' - - 'db/migrate/20130423180446_remove_author_from_stories.rb' - - 'db/migrate/20130425211008_add_setup_complete_to_user.rb' - - 'db/migrate/20130425222157_add_delayed_job.rb' - - 'db/migrate/20130429232127_add_status_to_feeds.rb' - - 'db/migrate/20130504005816_text_url.rb' - - 'db/migrate/20130504022615_change_story_permalink_column.rb' - - 'db/migrate/20130509131045_add_unique_constraints.rb' - - 'db/migrate/20130513025939_add_keep_unread_to_stories.rb' - - 'db/migrate/20130513044029_add_is_starred_status_for_stories.rb' - - 'db/migrate/20130522014405_add_api_key_to_user.rb' - - 'db/migrate/20130730120312_add_entry_id_to_stories.rb' - - 'db/migrate/20130805113712_update_stories_unique_constraints.rb' - - 'db/migrate/20130821020313_update_nil_entry_ids.rb' - - 'db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb' - - 'db/migrate/20140413100725_add_groups_table_and_foreign_keys_to_feeds.rb' - - 'db/migrate/20140421224454_fix_invalid_unicode.rb' - - 'db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb' - - 'fever_api.rb' - - 'spec/app_spec.rb' - - 'spec/commands/feeds/add_new_feed_spec.rb' - - 'spec/commands/feeds/export_to_opml_spec.rb' - - 'spec/commands/feeds/import_from_opml_spec.rb' - - 'spec/commands/find_new_stories_spec.rb' - - 'spec/commands/stories/mark_all_as_read_spec.rb' - - 'spec/commands/stories/mark_as_read_spec.rb' - - 'spec/commands/stories/mark_as_starred_spec.rb' - - 'spec/commands/stories/mark_as_unread_spec.rb' - - 'spec/commands/stories/mark_as_unstarred_spec.rb' - - 'spec/commands/stories/mark_feed_as_read_spec.rb' - - 'spec/commands/stories/mark_group_as_read_spec.rb' - - 'spec/commands/users/change_user_password_spec.rb' - - 'spec/commands/users/complete_setup_spec.rb' - - 'spec/commands/users/create_user_spec.rb' - - 'spec/commands/users/sign_in_user_spec.rb' - - 'spec/config/asset_pipeline_spec.rb' - - 'spec/controllers/debug_controller_spec.rb' - - 'spec/controllers/exports_controller_spec.rb' - - 'spec/controllers/feeds_controller_spec.rb' - - 'spec/controllers/first_run_controller_spec.rb' - - 'spec/controllers/imports_controller_spec.rb' - - 'spec/controllers/sessions_controller_spec.rb' - - 'spec/controllers/stories_controller_spec.rb' - - 'spec/factories.rb' - - 'spec/factories/feeds.rb' - - 'spec/factories/groups.rb' - - 'spec/factories/stories.rb' - - 'spec/factories/users.rb' - - 'spec/fever_api/authentication_spec.rb' - - 'spec/fever_api/read_favicons_spec.rb' - - 'spec/fever_api/read_feeds_groups_spec.rb' - - 'spec/fever_api/read_feeds_spec.rb' - - 'spec/fever_api/read_groups_spec.rb' - - 'spec/fever_api/read_items_spec.rb' - - 'spec/fever_api/read_links_spec.rb' - - 'spec/fever_api/sync_saved_item_ids_spec.rb' - - 'spec/fever_api/sync_unread_item_ids_spec.rb' - - 'spec/fever_api/write_mark_feed_spec.rb' - - 'spec/fever_api/write_mark_group_spec.rb' - - 'spec/fever_api/write_mark_item_spec.rb' - - 'spec/fever_api_spec.rb' - - 'spec/helpers/authentications_helper_spec.rb' - - 'spec/helpers/url_helpers_spec.rb' - - 'spec/integration/feed_importing_spec.rb' - - 'spec/javascript/test_controller.rb' - - 'spec/models/feed_spec.rb' - - 'spec/models/group_spec.rb' - - 'spec/models/migration_status_spec.rb' - - 'spec/models/story_spec.rb' - - 'spec/repositories/feed_repository_spec.rb' - - 'spec/repositories/group_repository_spec.rb' - - 'spec/repositories/story_repository_spec.rb' - - 'spec/repositories/user_repository_spec.rb' - - 'spec/spec_helper.rb' - - 'spec/support/active_record.rb' - - 'spec/support/coverage.rb' - - 'spec/support/feed_server.rb' - - 'spec/tasks/change_password_spec.rb' - - 'spec/tasks/fetch_feed_spec.rb' - - 'spec/tasks/fetch_feeds_spec.rb' - - 'spec/tasks/remove_old_stories_spec.rb' - - 'spec/utils/content_sanitizer_spec.rb' - - 'spec/utils/feed_discovery_spec.rb' - - 'spec/utils/i18n_support_spec.rb' - - 'spec/utils/opml_parser_spec.rb' - # Offense count: 86 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. diff --git a/Gemfile b/Gemfile index 18c1bba94..4153f7c03 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ruby_version_file = File.expand_path(".ruby-version", __dir__) ruby File.read(ruby_version_file).chomp if File.readable?(ruby_version_file) source "https://rubygems.org" diff --git a/Rakefile b/Rakefile index 4e4a44a3e..4bc059263 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler" Bundler.setup diff --git a/app.rb b/app.rb index 4baa6c30a..87db2d0c4 100644 --- a/app.rb +++ b/app.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "action_pack" require "action_view" require "action_controller" diff --git a/app/commands/feeds/add_new_feed.rb b/app/commands/feeds/add_new_feed.rb index 0b13f2eb8..ab0a56348 100644 --- a/app/commands/feeds/add_new_feed.rb +++ b/app/commands/feeds/add_new_feed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../models/feed" require_relative "../../utils/content_sanitizer" require_relative "../../utils/feed_discovery" diff --git a/app/commands/feeds/export_to_opml.rb b/app/commands/feeds/export_to_opml.rb index a8e6e457e..b6a859089 100644 --- a/app/commands/feeds/export_to_opml.rb +++ b/app/commands/feeds/export_to_opml.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "nokogiri" class ExportToOpml diff --git a/app/commands/feeds/find_new_stories.rb b/app/commands/feeds/find_new_stories.rb index 0aabad8f9..90798d694 100644 --- a/app/commands/feeds/find_new_stories.rb +++ b/app/commands/feeds/find_new_stories.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../repositories/story_repository" class FindNewStories diff --git a/app/commands/feeds/import_from_opml.rb b/app/commands/feeds/import_from_opml.rb index a0d911cc8..bfec71658 100644 --- a/app/commands/feeds/import_from_opml.rb +++ b/app/commands/feeds/import_from_opml.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../models/feed" require_relative "../../models/group" require_relative "../../utils/opml_parser" diff --git a/app/commands/stories/mark_all_as_read.rb b/app/commands/stories/mark_all_as_read.rb index e458c1f02..a74dceaf9 100644 --- a/app/commands/stories/mark_all_as_read.rb +++ b/app/commands/stories/mark_all_as_read.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../repositories/story_repository" class MarkAllAsRead diff --git a/app/commands/stories/mark_as_read.rb b/app/commands/stories/mark_as_read.rb index a2ddb121a..065ae1de5 100644 --- a/app/commands/stories/mark_as_read.rb +++ b/app/commands/stories/mark_as_read.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../repositories/story_repository" class MarkAsRead diff --git a/app/commands/stories/mark_as_starred.rb b/app/commands/stories/mark_as_starred.rb index e20ba4f0a..6f23c01ce 100644 --- a/app/commands/stories/mark_as_starred.rb +++ b/app/commands/stories/mark_as_starred.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../repositories/story_repository" class MarkAsStarred diff --git a/app/commands/stories/mark_as_unread.rb b/app/commands/stories/mark_as_unread.rb index a16791501..b03b45d90 100644 --- a/app/commands/stories/mark_as_unread.rb +++ b/app/commands/stories/mark_as_unread.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../repositories/story_repository" class MarkAsUnread diff --git a/app/commands/stories/mark_as_unstarred.rb b/app/commands/stories/mark_as_unstarred.rb index 4a616a25a..22cb666a3 100644 --- a/app/commands/stories/mark_as_unstarred.rb +++ b/app/commands/stories/mark_as_unstarred.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../repositories/story_repository" class MarkAsUnstarred diff --git a/app/commands/stories/mark_feed_as_read.rb b/app/commands/stories/mark_feed_as_read.rb index 8c28b8dbe..f65c6ba66 100644 --- a/app/commands/stories/mark_feed_as_read.rb +++ b/app/commands/stories/mark_feed_as_read.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../repositories/story_repository" class MarkFeedAsRead diff --git a/app/commands/stories/mark_group_as_read.rb b/app/commands/stories/mark_group_as_read.rb index c177e49b6..9364b3271 100644 --- a/app/commands/stories/mark_group_as_read.rb +++ b/app/commands/stories/mark_group_as_read.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../repositories/story_repository" class MarkGroupAsRead diff --git a/app/commands/users/change_user_password.rb b/app/commands/users/change_user_password.rb index a818aea13..38c57d904 100644 --- a/app/commands/users/change_user_password.rb +++ b/app/commands/users/change_user_password.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../repositories/user_repository" require_relative "../../utils/api_key" diff --git a/app/commands/users/complete_setup.rb b/app/commands/users/complete_setup.rb index e37cb80bf..37576c1ce 100644 --- a/app/commands/users/complete_setup.rb +++ b/app/commands/users/complete_setup.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CompleteSetup def self.complete(user) user.setup_complete = true diff --git a/app/commands/users/create_user.rb b/app/commands/users/create_user.rb index c295500f1..b51d6eddb 100644 --- a/app/commands/users/create_user.rb +++ b/app/commands/users/create_user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../utils/api_key" class CreateUser diff --git a/app/commands/users/sign_in_user.rb b/app/commands/users/sign_in_user.rb index f41ad2e98..a0cb5aea2 100644 --- a/app/commands/users/sign_in_user.rb +++ b/app/commands/users/sign_in_user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../models/user" class SignInUser diff --git a/app/controllers/debug_controller.rb b/app/controllers/debug_controller.rb index feece9b69..44a82b01f 100644 --- a/app/controllers/debug_controller.rb +++ b/app/controllers/debug_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../models/migration_status" class DebugController < ApplicationController diff --git a/app/controllers/sinatra/first_run_controller.rb b/app/controllers/sinatra/first_run_controller.rb index 79eb8f58d..78e5bf5df 100644 --- a/app/controllers/sinatra/first_run_controller.rb +++ b/app/controllers/sinatra/first_run_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../commands/feeds/import_from_opml" require_relative "../../commands/users/create_user" require_relative "../../commands/users/complete_setup" diff --git a/app/controllers/sinatra/sessions_controller.rb b/app/controllers/sinatra/sessions_controller.rb index bd305b390..e8c16a869 100644 --- a/app/controllers/sinatra/sessions_controller.rb +++ b/app/controllers/sinatra/sessions_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../commands/users/sign_in_user" class Stringer < Sinatra::Base diff --git a/app/controllers/sinatra/stories_controller.rb b/app/controllers/sinatra/stories_controller.rb index bf9cb3049..9614f86d1 100644 --- a/app/controllers/sinatra/stories_controller.rb +++ b/app/controllers/sinatra/stories_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../repositories/story_repository" require_relative "../../commands/stories/mark_all_as_read" diff --git a/app/fever_api/authentication.rb b/app/fever_api/authentication.rb index 819eb07eb..05f55056a 100644 --- a/app/fever_api/authentication.rb +++ b/app/fever_api/authentication.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module FeverAPI class Authentication def initialize(options = {}) diff --git a/app/fever_api/read_favicons.rb b/app/fever_api/read_favicons.rb index c924b7cfd..6b5a8dc58 100644 --- a/app/fever_api/read_favicons.rb +++ b/app/fever_api/read_favicons.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module FeverAPI class ReadFavicons - ICON = "R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==".freeze + ICON = "R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" def call(params = {}) if params.keys.include?("favicons") diff --git a/app/fever_api/read_feeds.rb b/app/fever_api/read_feeds.rb index a00a4b60d..cabba321f 100644 --- a/app/fever_api/read_feeds.rb +++ b/app/fever_api/read_feeds.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../repositories/feed_repository" module FeverAPI diff --git a/app/fever_api/read_feeds_groups.rb b/app/fever_api/read_feeds_groups.rb index 13cec3b93..d6597a293 100644 --- a/app/fever_api/read_feeds_groups.rb +++ b/app/fever_api/read_feeds_groups.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../repositories/feed_repository" module FeverAPI diff --git a/app/fever_api/read_groups.rb b/app/fever_api/read_groups.rb index 2709e6648..2540195f1 100644 --- a/app/fever_api/read_groups.rb +++ b/app/fever_api/read_groups.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../repositories/group_repository" module FeverAPI diff --git a/app/fever_api/read_items.rb b/app/fever_api/read_items.rb index ca26d04b9..4d65ba4f7 100644 --- a/app/fever_api/read_items.rb +++ b/app/fever_api/read_items.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../repositories/story_repository" module FeverAPI diff --git a/app/fever_api/read_links.rb b/app/fever_api/read_links.rb index 595343c9a..1377b5b15 100644 --- a/app/fever_api/read_links.rb +++ b/app/fever_api/read_links.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module FeverAPI class ReadLinks def call(params = {}) diff --git a/app/fever_api/response.rb b/app/fever_api/response.rb index 31336b5f4..327c40f55 100644 --- a/app/fever_api/response.rb +++ b/app/fever_api/response.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "authentication" require_relative "read_groups" diff --git a/app/fever_api/sync_saved_item_ids.rb b/app/fever_api/sync_saved_item_ids.rb index 9e4ff9dde..e169a1c5e 100644 --- a/app/fever_api/sync_saved_item_ids.rb +++ b/app/fever_api/sync_saved_item_ids.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../repositories/story_repository" module FeverAPI diff --git a/app/fever_api/sync_unread_item_ids.rb b/app/fever_api/sync_unread_item_ids.rb index 7d469d73b..84a043a83 100644 --- a/app/fever_api/sync_unread_item_ids.rb +++ b/app/fever_api/sync_unread_item_ids.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../repositories/story_repository" module FeverAPI diff --git a/app/fever_api/write_mark_feed.rb b/app/fever_api/write_mark_feed.rb index b4b582e6b..b6bd4347a 100644 --- a/app/fever_api/write_mark_feed.rb +++ b/app/fever_api/write_mark_feed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../commands/stories/mark_feed_as_read" module FeverAPI diff --git a/app/fever_api/write_mark_group.rb b/app/fever_api/write_mark_group.rb index 7ccbe127b..773cfc4f2 100644 --- a/app/fever_api/write_mark_group.rb +++ b/app/fever_api/write_mark_group.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../commands/stories/mark_group_as_read" module FeverAPI diff --git a/app/fever_api/write_mark_item.rb b/app/fever_api/write_mark_item.rb index 7d35e31db..058da471c 100644 --- a/app/fever_api/write_mark_item.rb +++ b/app/fever_api/write_mark_item.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../commands/stories/mark_as_read" require_relative "../commands/stories/mark_as_unread" require_relative "../commands/stories/mark_as_starred" diff --git a/app/helpers/authentication_helpers.rb b/app/helpers/authentication_helpers.rb index 93e0eb364..9fa9705a9 100644 --- a/app/helpers/authentication_helpers.rb +++ b/app/helpers/authentication_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "sinatra/base" require_relative "../repositories/user_repository" diff --git a/app/helpers/url_helpers.rb b/app/helpers/url_helpers.rb index b9e0121f9..3bcd0348a 100644 --- a/app/helpers/url_helpers.rb +++ b/app/helpers/url_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "nokogiri" require "uri" diff --git a/app/jobs/fetch_feed_job.rb b/app/jobs/fetch_feed_job.rb index 77bedc4d1..88294c3d7 100644 --- a/app/jobs/fetch_feed_job.rb +++ b/app/jobs/fetch_feed_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + FetchFeedJob = Struct.new(:feed_id) do def perform diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 10a4cba84..71fbba5b3 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end diff --git a/app/models/feed.rb b/app/models/feed.rb index 1f7913c4f..3e4716405 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "./application_record" class Feed < ApplicationRecord diff --git a/app/models/group.rb b/app/models/group.rb index e8d8ee22b..3dbcf1b96 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "./application_record" class Group < ApplicationRecord diff --git a/app/models/migration_status.rb b/app/models/migration_status.rb index a358ad49d..bda127c81 100644 --- a/app/models/migration_status.rb +++ b/app/models/migration_status.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MigrationStatus attr_reader :migrator diff --git a/app/models/story.rb b/app/models/story.rb index ae346ef2a..ea3055e79 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "./application_record" require_relative "./feed" @@ -6,7 +8,7 @@ class Story < ApplicationRecord validates_uniqueness_of :entry_id, scope: :feed_id - UNTITLED = "[untitled]".freeze + UNTITLED = "[untitled]" def headline title.nil? ? UNTITLED : strip_html(title)[0, 50] diff --git a/app/models/user.rb b/app/models/user.rb index dcbec3ad5..4b45e352c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "./application_record" class User < ApplicationRecord diff --git a/app/repositories/feed_repository.rb b/app/repositories/feed_repository.rb index 5deee8e22..d453fa61f 100644 --- a/app/repositories/feed_repository.rb +++ b/app/repositories/feed_repository.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../models/feed" class FeedRepository diff --git a/app/repositories/group_repository.rb b/app/repositories/group_repository.rb index b827dd199..95bd01570 100644 --- a/app/repositories/group_repository.rb +++ b/app/repositories/group_repository.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../models/group" class GroupRepository diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index b6d8a7e47..606ba003a 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../helpers/url_helpers" require_relative "../models/story" require_relative "../utils/content_sanitizer" diff --git a/app/repositories/user_repository.rb b/app/repositories/user_repository.rb index 029f26b85..a0292dd07 100644 --- a/app/repositories/user_repository.rb +++ b/app/repositories/user_repository.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../models/user" class UserRepository diff --git a/app/tasks/change_password.rb b/app/tasks/change_password.rb index 0a16acbfe..b59e6fb30 100644 --- a/app/tasks/change_password.rb +++ b/app/tasks/change_password.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "io/console" require_relative "../commands/users/change_user_password" diff --git a/app/tasks/fetch_feed.rb b/app/tasks/fetch_feed.rb index aed06a496..5278f1423 100644 --- a/app/tasks/fetch_feed.rb +++ b/app/tasks/fetch_feed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "feedjira" require "httparty" diff --git a/app/tasks/fetch_feeds.rb b/app/tasks/fetch_feeds.rb index bb7867525..8a45c01c9 100644 --- a/app/tasks/fetch_feeds.rb +++ b/app/tasks/fetch_feeds.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "thread/pool" require_relative "fetch_feed" diff --git a/app/tasks/remove_old_stories.rb b/app/tasks/remove_old_stories.rb index d766b49bd..4fd1f43fb 100644 --- a/app/tasks/remove_old_stories.rb +++ b/app/tasks/remove_old_stories.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RemoveOldStories def self.remove!(number_of_days) stories = old_stories(number_of_days) diff --git a/app/utils/api_key.rb b/app/utils/api_key.rb index 6f0f52c4c..397563b6e 100644 --- a/app/utils/api_key.rb +++ b/app/utils/api_key.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "digest/md5" class ApiKey diff --git a/app/utils/content_sanitizer.rb b/app/utils/content_sanitizer.rb index 87886c688..e0bf75b8d 100644 --- a/app/utils/content_sanitizer.rb +++ b/app/utils/content_sanitizer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ContentSanitizer def self.sanitize(content) Loofah.fragment(content.gsub(//i, "")) diff --git a/app/utils/feed_discovery.rb b/app/utils/feed_discovery.rb index 83f4d5a53..cb1baadaa 100644 --- a/app/utils/feed_discovery.rb +++ b/app/utils/feed_discovery.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "feedbag" require "feedjira" require "httparty" diff --git a/app/utils/opml_parser.rb b/app/utils/opml_parser.rb index e99d45938..6b197380e 100644 --- a/app/utils/opml_parser.rb +++ b/app/utils/opml_parser.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "nokogiri" class OpmlParser diff --git a/app/utils/sample_story.rb b/app/utils/sample_story.rb index 469465990..6b67f6fbe 100644 --- a/app/utils/sample_story.rb +++ b/app/utils/sample_story.rb @@ -1,4 +1,6 @@ -SAMPLE_BODY = <<~EOS.freeze +# frozen_string_literal: true + +SAMPLE_BODY = <<~EOS

    Tofu shoreditch intelligentsia umami, fashion axe photo booth try-hard terry richardson quinoa actually fingerstache meggings fixie. Aesthetic salvia vinyl raw denim, keffiyeh master cleanse tonx selfies mlkshk occupy twee diff --git a/config.ru b/config.ru index a45d484f3..d4aa7984e 100644 --- a/config.ru +++ b/config.ru @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rubygems" require "bundler" diff --git a/config/asset_pipeline.rb b/config/asset_pipeline.rb index 59b009e36..569219604 100644 --- a/config/asset_pipeline.rb +++ b/config/asset_pipeline.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module AssetPipeline def registered(app) app.set :sprockets, Sprockets::Environment.new(app.root) diff --git a/config/puma.rb b/config/puma.rb index dd0037c77..e439e2ca5 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + workers workers Integer(ENV.fetch("WEB_CONCURRENCY", 1)) threads_count = Integer(ENV.fetch("MAX_THREADS", 2)) threads threads_count, threads_count diff --git a/db/migrate/20130409010818_create_feeds.rb b/db/migrate/20130409010818_create_feeds.rb index 628400da3..26b2ca2e0 100644 --- a/db/migrate/20130409010818_create_feeds.rb +++ b/db/migrate/20130409010818_create_feeds.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateFeeds < ActiveRecord::Migration[4.2] def change create_table :feeds do |t| diff --git a/db/migrate/20130409010826_create_stories.rb b/db/migrate/20130409010826_create_stories.rb index 8df287917..73b494077 100644 --- a/db/migrate/20130409010826_create_stories.rb +++ b/db/migrate/20130409010826_create_stories.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateStories < ActiveRecord::Migration[4.2] def change create_table :stories do |t| diff --git a/db/migrate/20130412185253_add_new_fields_to_stories.rb b/db/migrate/20130412185253_add_new_fields_to_stories.rb index 00beff1cc..e3c95eda9 100644 --- a/db/migrate/20130412185253_add_new_fields_to_stories.rb +++ b/db/migrate/20130412185253_add_new_fields_to_stories.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddNewFieldsToStories < ActiveRecord::Migration[4.2] def change add_column :stories, :published, :timestamp diff --git a/db/migrate/20130418221144_add_user_model.rb b/db/migrate/20130418221144_add_user_model.rb index 5adc4fc3b..6c8ccb858 100644 --- a/db/migrate/20130418221144_add_user_model.rb +++ b/db/migrate/20130418221144_add_user_model.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddUserModel < ActiveRecord::Migration[4.2] def change create_table :users do |t| diff --git a/db/migrate/20130423001740_drop_email_from_user.rb b/db/migrate/20130423001740_drop_email_from_user.rb index f40b8a62c..9579bdfa7 100644 --- a/db/migrate/20130423001740_drop_email_from_user.rb +++ b/db/migrate/20130423001740_drop_email_from_user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DropEmailFromUser < ActiveRecord::Migration[4.2] def up remove_column :users, :email diff --git a/db/migrate/20130423180446_remove_author_from_stories.rb b/db/migrate/20130423180446_remove_author_from_stories.rb index 47b630d0f..37061a611 100644 --- a/db/migrate/20130423180446_remove_author_from_stories.rb +++ b/db/migrate/20130423180446_remove_author_from_stories.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RemoveAuthorFromStories < ActiveRecord::Migration[4.2] def up remove_column :stories, :author diff --git a/db/migrate/20130425211008_add_setup_complete_to_user.rb b/db/migrate/20130425211008_add_setup_complete_to_user.rb index d98aa7a1c..39f29aacb 100644 --- a/db/migrate/20130425211008_add_setup_complete_to_user.rb +++ b/db/migrate/20130425211008_add_setup_complete_to_user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddSetupCompleteToUser < ActiveRecord::Migration[4.2] def change add_column :users, :setup_complete, :boolean diff --git a/db/migrate/20130425222157_add_delayed_job.rb b/db/migrate/20130425222157_add_delayed_job.rb index 60736b629..d4afefab8 100644 --- a/db/migrate/20130425222157_add_delayed_job.rb +++ b/db/migrate/20130425222157_add_delayed_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddDelayedJob < ActiveRecord::Migration[4.2] def self.up create_table :delayed_jobs, force: true do |table| diff --git a/db/migrate/20130429232127_add_status_to_feeds.rb b/db/migrate/20130429232127_add_status_to_feeds.rb index 9bb84a23f..a458e8528 100644 --- a/db/migrate/20130429232127_add_status_to_feeds.rb +++ b/db/migrate/20130429232127_add_status_to_feeds.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddStatusToFeeds < ActiveRecord::Migration[4.2] def change add_column :feeds, :status, :int diff --git a/db/migrate/20130504005816_text_url.rb b/db/migrate/20130504005816_text_url.rb index 5f3c1ea7a..57e02d5cb 100644 --- a/db/migrate/20130504005816_text_url.rb +++ b/db/migrate/20130504005816_text_url.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TextUrl < ActiveRecord::Migration[4.2] def up change_column :feeds, :url, :text diff --git a/db/migrate/20130504022615_change_story_permalink_column.rb b/db/migrate/20130504022615_change_story_permalink_column.rb index db8d8b924..503a2dbd9 100644 --- a/db/migrate/20130504022615_change_story_permalink_column.rb +++ b/db/migrate/20130504022615_change_story_permalink_column.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ChangeStoryPermalinkColumn < ActiveRecord::Migration[4.2] def up change_column :stories, :permalink, :text diff --git a/db/migrate/20130509131045_add_unique_constraints.rb b/db/migrate/20130509131045_add_unique_constraints.rb index f04989164..270dde2ba 100644 --- a/db/migrate/20130509131045_add_unique_constraints.rb +++ b/db/migrate/20130509131045_add_unique_constraints.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddUniqueConstraints < ActiveRecord::Migration[4.2] def change add_index :stories, [:permalink, :feed_id], unique: true diff --git a/db/migrate/20130513025939_add_keep_unread_to_stories.rb b/db/migrate/20130513025939_add_keep_unread_to_stories.rb index 76742a0de..98b42cdf1 100644 --- a/db/migrate/20130513025939_add_keep_unread_to_stories.rb +++ b/db/migrate/20130513025939_add_keep_unread_to_stories.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddKeepUnreadToStories < ActiveRecord::Migration[4.2] def change add_column :stories, :keep_unread, :boolean, default: false diff --git a/db/migrate/20130513044029_add_is_starred_status_for_stories.rb b/db/migrate/20130513044029_add_is_starred_status_for_stories.rb index 8333225a0..4d64c1b19 100644 --- a/db/migrate/20130513044029_add_is_starred_status_for_stories.rb +++ b/db/migrate/20130513044029_add_is_starred_status_for_stories.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddIsStarredStatusForStories < ActiveRecord::Migration[4.2] def change add_column :stories, :is_starred, :boolean, default: false diff --git a/db/migrate/20130522014405_add_api_key_to_user.rb b/db/migrate/20130522014405_add_api_key_to_user.rb index b52aae725..a193761da 100644 --- a/db/migrate/20130522014405_add_api_key_to_user.rb +++ b/db/migrate/20130522014405_add_api_key_to_user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddApiKeyToUser < ActiveRecord::Migration[4.2] def change add_column :users, :api_key, :string diff --git a/db/migrate/20130730120312_add_entry_id_to_stories.rb b/db/migrate/20130730120312_add_entry_id_to_stories.rb index f160a6d9d..fe25c77ed 100644 --- a/db/migrate/20130730120312_add_entry_id_to_stories.rb +++ b/db/migrate/20130730120312_add_entry_id_to_stories.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddEntryIdToStories < ActiveRecord::Migration[4.2] def change add_column :stories, :entry_id, :string diff --git a/db/migrate/20130805113712_update_stories_unique_constraints.rb b/db/migrate/20130805113712_update_stories_unique_constraints.rb index 2c320babb..326049b16 100644 --- a/db/migrate/20130805113712_update_stories_unique_constraints.rb +++ b/db/migrate/20130805113712_update_stories_unique_constraints.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class UpdateStoriesUniqueConstraints < ActiveRecord::Migration[4.2] def up remove_index :stories, [:permalink, :feed_id] diff --git a/db/migrate/20130821020313_update_nil_entry_ids.rb b/db/migrate/20130821020313_update_nil_entry_ids.rb index 2b5624910..1f3a90416 100644 --- a/db/migrate/20130821020313_update_nil_entry_ids.rb +++ b/db/migrate/20130821020313_update_nil_entry_ids.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class UpdateNilEntryIds < ActiveRecord::Migration[4.2] def up Story.where(entry_id: nil).each do |story| diff --git a/db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb b/db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb index 0d847e58c..b2ad28328 100644 --- a/db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb +++ b/db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class UseTextDatatypeForTitleAndEntryId < ActiveRecord::Migration[4.2] def up change_column :stories, :title, :text diff --git a/db/migrate/20140413100725_add_groups_table_and_foreign_keys_to_feeds.rb b/db/migrate/20140413100725_add_groups_table_and_foreign_keys_to_feeds.rb index 149e3f77b..d862f5972 100644 --- a/db/migrate/20140413100725_add_groups_table_and_foreign_keys_to_feeds.rb +++ b/db/migrate/20140413100725_add_groups_table_and_foreign_keys_to_feeds.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddGroupsTableAndForeignKeysToFeeds < ActiveRecord::Migration[4.2] def up create_table :groups do |t| diff --git a/db/migrate/20140421224454_fix_invalid_unicode.rb b/db/migrate/20140421224454_fix_invalid_unicode.rb index 902e311d2..2f556a154 100644 --- a/db/migrate/20140421224454_fix_invalid_unicode.rb +++ b/db/migrate/20140421224454_fix_invalid_unicode.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class FixInvalidUnicode < ActiveRecord::Migration[4.2] def up Story.find_each do |story| diff --git a/db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb b/db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb index 5d0fa90d4..bea9b1a2f 100644 --- a/db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb +++ b/db/migrate/20141102103617_fix_invalid_titles_with_unicode_line_endings.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class FixInvalidTitlesWithUnicodeLineEndings < ActiveRecord::Migration[4.2] def up Story.find_each do |story| diff --git a/fever_api.rb b/fever_api.rb index e3d7f7cf4..2c639ff12 100644 --- a/fever_api.rb +++ b/fever_api.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "sinatra/base" require "sinatra/activerecord" diff --git a/spec/app_spec.rb b/spec/app_spec.rb index 0d79af4cf..2f8616302 100644 --- a/spec/app_spec.rb +++ b/spec/app_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "support/active_record" diff --git a/spec/commands/feeds/add_new_feed_spec.rb b/spec/commands/feeds/add_new_feed_spec.rb index 2c0c77d93..e0eb8d39d 100644 --- a/spec/commands/feeds/add_new_feed_spec.rb +++ b/spec/commands/feeds/add_new_feed_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "/commands/feeds/add_new_feed" diff --git a/spec/commands/feeds/export_to_opml_spec.rb b/spec/commands/feeds/export_to_opml_spec.rb index 259fa09e7..1ed1ec622 100644 --- a/spec/commands/feeds/export_to_opml_spec.rb +++ b/spec/commands/feeds/export_to_opml_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "commands/feeds/export_to_opml" diff --git a/spec/commands/feeds/import_from_opml_spec.rb b/spec/commands/feeds/import_from_opml_spec.rb index 8a3386b1d..0b4a08643 100644 --- a/spec/commands/feeds/import_from_opml_spec.rb +++ b/spec/commands/feeds/import_from_opml_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "commands/feeds/import_from_opml" diff --git a/spec/commands/find_new_stories_spec.rb b/spec/commands/find_new_stories_spec.rb index c6d542a30..8f4af7c98 100644 --- a/spec/commands/find_new_stories_spec.rb +++ b/spec/commands/find_new_stories_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "repositories/story_repository" diff --git a/spec/commands/stories/mark_all_as_read_spec.rb b/spec/commands/stories/mark_all_as_read_spec.rb index b72c75b78..656302f9c 100644 --- a/spec/commands/stories/mark_all_as_read_spec.rb +++ b/spec/commands/stories/mark_all_as_read_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "commands/stories/mark_all_as_read" diff --git a/spec/commands/stories/mark_as_read_spec.rb b/spec/commands/stories/mark_as_read_spec.rb index 7d10291ff..603e99e13 100644 --- a/spec/commands/stories/mark_as_read_spec.rb +++ b/spec/commands/stories/mark_as_read_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "commands/stories/mark_as_read" diff --git a/spec/commands/stories/mark_as_starred_spec.rb b/spec/commands/stories/mark_as_starred_spec.rb index 5413d80dd..150d78fe1 100644 --- a/spec/commands/stories/mark_as_starred_spec.rb +++ b/spec/commands/stories/mark_as_starred_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "commands/stories/mark_as_starred" diff --git a/spec/commands/stories/mark_as_unread_spec.rb b/spec/commands/stories/mark_as_unread_spec.rb index 0971ceb63..c5d72427b 100644 --- a/spec/commands/stories/mark_as_unread_spec.rb +++ b/spec/commands/stories/mark_as_unread_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "commands/stories/mark_as_unread" diff --git a/spec/commands/stories/mark_as_unstarred_spec.rb b/spec/commands/stories/mark_as_unstarred_spec.rb index b1d06be36..5802c752f 100644 --- a/spec/commands/stories/mark_as_unstarred_spec.rb +++ b/spec/commands/stories/mark_as_unstarred_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "commands/stories/mark_as_unstarred" diff --git a/spec/commands/stories/mark_feed_as_read_spec.rb b/spec/commands/stories/mark_feed_as_read_spec.rb index 00e5dff69..72d586e87 100644 --- a/spec/commands/stories/mark_feed_as_read_spec.rb +++ b/spec/commands/stories/mark_feed_as_read_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "commands/stories/mark_feed_as_read" diff --git a/spec/commands/stories/mark_group_as_read_spec.rb b/spec/commands/stories/mark_group_as_read_spec.rb index 452b2446d..570e19127 100644 --- a/spec/commands/stories/mark_group_as_read_spec.rb +++ b/spec/commands/stories/mark_group_as_read_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "commands/stories/mark_group_as_read" diff --git a/spec/commands/users/change_user_password_spec.rb b/spec/commands/users/change_user_password_spec.rb index caae4e3ee..61a208e76 100644 --- a/spec/commands/users/change_user_password_spec.rb +++ b/spec/commands/users/change_user_password_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "support/active_record" diff --git a/spec/commands/users/complete_setup_spec.rb b/spec/commands/users/complete_setup_spec.rb index cc938a7e1..98fca9f1a 100644 --- a/spec/commands/users/complete_setup_spec.rb +++ b/spec/commands/users/complete_setup_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "commands/users/complete_setup" diff --git a/spec/commands/users/create_user_spec.rb b/spec/commands/users/create_user_spec.rb index 189f44900..6c93c7f4f 100644 --- a/spec/commands/users/create_user_spec.rb +++ b/spec/commands/users/create_user_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "commands/users/create_user" diff --git a/spec/commands/users/sign_in_user_spec.rb b/spec/commands/users/sign_in_user_spec.rb index f3f921810..7132a70e3 100644 --- a/spec/commands/users/sign_in_user_spec.rb +++ b/spec/commands/users/sign_in_user_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "commands/users/sign_in_user" diff --git a/spec/config/asset_pipeline_spec.rb b/spec/config/asset_pipeline_spec.rb index 4eb814211..fbd08855c 100644 --- a/spec/config/asset_pipeline_spec.rb +++ b/spec/config/asset_pipeline_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" describe "AssetPipeline" do diff --git a/spec/controllers/debug_controller_spec.rb b/spec/controllers/debug_controller_spec.rb index d5cae8cbc..f0c7dd4b7 100644 --- a/spec/controllers/debug_controller_spec.rb +++ b/spec/controllers/debug_controller_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "support/active_record" diff --git a/spec/controllers/exports_controller_spec.rb b/spec/controllers/exports_controller_spec.rb index f94891239..07285e56f 100644 --- a/spec/controllers/exports_controller_spec.rb +++ b/spec/controllers/exports_controller_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "controllers/exports_controller" diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index 574f3c729..bbc28e605 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "controllers/feeds_controller" diff --git a/spec/controllers/first_run_controller_spec.rb b/spec/controllers/first_run_controller_spec.rb index baf706c92..14c8e62d7 100644 --- a/spec/controllers/first_run_controller_spec.rb +++ b/spec/controllers/first_run_controller_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "support/active_record" diff --git a/spec/controllers/imports_controller_spec.rb b/spec/controllers/imports_controller_spec.rb index b62500cd4..2b052246e 100644 --- a/spec/controllers/imports_controller_spec.rb +++ b/spec/controllers/imports_controller_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "controllers/imports_controller" diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 85eda0ade..93216b5a7 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "controllers/sinatra/sessions_controller" diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index 0fcf3fa24..c06ba3702 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "will_paginate/array" diff --git a/spec/factories.rb b/spec/factories.rb index 49b48b372..57a1c15c9 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "factories/feeds" require_relative "factories/groups" require_relative "factories/stories" diff --git a/spec/factories/feeds.rb b/spec/factories/feeds.rb index a74858e7e..f722a6617 100644 --- a/spec/factories/feeds.rb +++ b/spec/factories/feeds.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + FactoryBot.define do factory(:feed) do sequence(:name, 100) { |n| "Feed #{n}" } diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index 21306b6bb..d51bca922 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -1 +1,3 @@ +# frozen_string_literal: true + FactoryBot.define { factory(:group) } diff --git a/spec/factories/stories.rb b/spec/factories/stories.rb index 22859cbb1..dbf19d7f1 100644 --- a/spec/factories/stories.rb +++ b/spec/factories/stories.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + FactoryBot.define do factory(:story) do feed diff --git a/spec/factories/users.rb b/spec/factories/users.rb index a3192ad95..53518a80a 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + FactoryBot.define do factory(:user) do password { "super-secret" } diff --git a/spec/fever_api/authentication_spec.rb b/spec/fever_api/authentication_spec.rb index 0c96932eb..4cfd295dc 100644 --- a/spec/fever_api/authentication_spec.rb +++ b/spec/fever_api/authentication_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "fever_api/authentication" diff --git a/spec/fever_api/read_favicons_spec.rb b/spec/fever_api/read_favicons_spec.rb index 93bb4efa1..19ce8a94b 100644 --- a/spec/fever_api/read_favicons_spec.rb +++ b/spec/fever_api/read_favicons_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "fever_api/read_favicons" diff --git a/spec/fever_api/read_feeds_groups_spec.rb b/spec/fever_api/read_feeds_groups_spec.rb index 6540503dd..4df971b5f 100644 --- a/spec/fever_api/read_feeds_groups_spec.rb +++ b/spec/fever_api/read_feeds_groups_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "fever_api/read_feeds_groups" diff --git a/spec/fever_api/read_feeds_spec.rb b/spec/fever_api/read_feeds_spec.rb index 5ab15483c..40732e031 100644 --- a/spec/fever_api/read_feeds_spec.rb +++ b/spec/fever_api/read_feeds_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "fever_api/read_feeds" diff --git a/spec/fever_api/read_groups_spec.rb b/spec/fever_api/read_groups_spec.rb index 6adc080ff..2b7b5951d 100644 --- a/spec/fever_api/read_groups_spec.rb +++ b/spec/fever_api/read_groups_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "fever_api/read_groups" diff --git a/spec/fever_api/read_items_spec.rb b/spec/fever_api/read_items_spec.rb index d146af044..a44a316f0 100644 --- a/spec/fever_api/read_items_spec.rb +++ b/spec/fever_api/read_items_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "fever_api/read_items" diff --git a/spec/fever_api/read_links_spec.rb b/spec/fever_api/read_links_spec.rb index 895123de0..3f279179d 100644 --- a/spec/fever_api/read_links_spec.rb +++ b/spec/fever_api/read_links_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "fever_api/read_links" diff --git a/spec/fever_api/sync_saved_item_ids_spec.rb b/spec/fever_api/sync_saved_item_ids_spec.rb index 5907df49a..6b8afc0f9 100644 --- a/spec/fever_api/sync_saved_item_ids_spec.rb +++ b/spec/fever_api/sync_saved_item_ids_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "fever_api/sync_saved_item_ids" diff --git a/spec/fever_api/sync_unread_item_ids_spec.rb b/spec/fever_api/sync_unread_item_ids_spec.rb index e7f98e9c8..8bb197dbc 100644 --- a/spec/fever_api/sync_unread_item_ids_spec.rb +++ b/spec/fever_api/sync_unread_item_ids_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "fever_api/sync_unread_item_ids" diff --git a/spec/fever_api/write_mark_feed_spec.rb b/spec/fever_api/write_mark_feed_spec.rb index 719b9ed65..2a6c252e4 100644 --- a/spec/fever_api/write_mark_feed_spec.rb +++ b/spec/fever_api/write_mark_feed_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "fever_api/write_mark_feed" diff --git a/spec/fever_api/write_mark_group_spec.rb b/spec/fever_api/write_mark_group_spec.rb index 9eaa6ee6a..b86b42f98 100644 --- a/spec/fever_api/write_mark_group_spec.rb +++ b/spec/fever_api/write_mark_group_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "fever_api/write_mark_group" diff --git a/spec/fever_api/write_mark_item_spec.rb b/spec/fever_api/write_mark_item_spec.rb index 9efd634cc..3edb48802 100644 --- a/spec/fever_api/write_mark_item_spec.rb +++ b/spec/fever_api/write_mark_item_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "fever_api/write_mark_item" diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index 9f656ed3e..aa99b0df5 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "./fever_api" diff --git a/spec/helpers/authentications_helper_spec.rb b/spec/helpers/authentications_helper_spec.rb index 7d77141df..180082dc1 100644 --- a/spec/helpers/authentications_helper_spec.rb +++ b/spec/helpers/authentications_helper_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "helpers/authentication_helpers" diff --git a/spec/helpers/url_helpers_spec.rb b/spec/helpers/url_helpers_spec.rb index edcb36d63..4c8561efd 100644 --- a/spec/helpers/url_helpers_spec.rb +++ b/spec/helpers/url_helpers_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "helpers/url_helpers" diff --git a/spec/integration/feed_importing_spec.rb b/spec/integration/feed_importing_spec.rb index 200e0bb3d..1831528db 100644 --- a/spec/integration/feed_importing_spec.rb +++ b/spec/integration/feed_importing_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "time" require "support/active_record" diff --git a/spec/javascript/test_controller.rb b/spec/javascript/test_controller.rb index 84a5f6b59..470757949 100644 --- a/spec/javascript/test_controller.rb +++ b/spec/javascript/test_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Stringer < Sinatra::Base def self.test_path(*chunks) File.expand_path(File.join("..", *chunks), __FILE__) diff --git a/spec/models/feed_spec.rb b/spec/models/feed_spec.rb index e7e3b4c57..7acdc0501 100644 --- a/spec/models/feed_spec.rb +++ b/spec/models/feed_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "support/active_record" diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 14b3285ca..6556e076b 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "support/active_record" diff --git a/spec/models/migration_status_spec.rb b/spec/models/migration_status_spec.rb index d32f79683..9335c56eb 100644 --- a/spec/models/migration_status_spec.rb +++ b/spec/models/migration_status_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "support/active_record" diff --git a/spec/models/story_spec.rb b/spec/models/story_spec.rb index 27fb71ac0..cc6729698 100644 --- a/spec/models/story_spec.rb +++ b/spec/models/story_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "support/active_record" diff --git a/spec/repositories/feed_repository_spec.rb b/spec/repositories/feed_repository_spec.rb index 2f2dac5d4..1bd31867c 100644 --- a/spec/repositories/feed_repository_spec.rb +++ b/spec/repositories/feed_repository_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "support/active_record" diff --git a/spec/repositories/group_repository_spec.rb b/spec/repositories/group_repository_spec.rb index fd119fa27..2f6a5a827 100644 --- a/spec/repositories/group_repository_spec.rb +++ b/spec/repositories/group_repository_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "support/active_record" diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index bc8f96949..98aa3fada 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "support/active_record" diff --git a/spec/repositories/user_repository_spec.rb b/spec/repositories/user_repository_spec.rb index 2e969b000..8e51de51b 100644 --- a/spec/repositories/user_repository_spec.rb +++ b/spec/repositories/user_repository_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "support/active_record" diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6873a783f..9688013bb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ENV["RACK_ENV"] = "test" ENV["ENFORCE_SSL"] = "true" diff --git a/spec/support/active_record.rb b/spec/support/active_record.rb index 724b89aa0..92ac0b8f6 100644 --- a/spec/support/active_record.rb +++ b/spec/support/active_record.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_record" db_config = YAML.safe_load(File.read("config/database.yml")) diff --git a/spec/support/coverage.rb b/spec/support/coverage.rb index 2f2a2b83b..6c914f365 100644 --- a/spec/support/coverage.rb +++ b/spec/support/coverage.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "simplecov" if ENV["CI"] diff --git a/spec/support/feed_server.rb b/spec/support/feed_server.rb index 502c9b338..0fd60f524 100644 --- a/spec/support/feed_server.rb +++ b/spec/support/feed_server.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class FeedServer attr_writer :response diff --git a/spec/tasks/change_password_spec.rb b/spec/tasks/change_password_spec.rb index 168f5401a..648a64858 100644 --- a/spec/tasks/change_password_spec.rb +++ b/spec/tasks/change_password_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "tasks/change_password" diff --git a/spec/tasks/fetch_feed_spec.rb b/spec/tasks/fetch_feed_spec.rb index a5bc4d88b..0a07c6df0 100644 --- a/spec/tasks/fetch_feed_spec.rb +++ b/spec/tasks/fetch_feed_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "tasks/fetch_feed" diff --git a/spec/tasks/fetch_feeds_spec.rb b/spec/tasks/fetch_feeds_spec.rb index 7b66229ad..055b394f6 100644 --- a/spec/tasks/fetch_feeds_spec.rb +++ b/spec/tasks/fetch_feeds_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "support/active_record" diff --git a/spec/tasks/remove_old_stories_spec.rb b/spec/tasks/remove_old_stories_spec.rb index 2bba7bc47..7680edd23 100644 --- a/spec/tasks/remove_old_stories_spec.rb +++ b/spec/tasks/remove_old_stories_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "tasks/remove_old_stories" diff --git a/spec/utils/content_sanitizer_spec.rb b/spec/utils/content_sanitizer_spec.rb index a5056fb12..7d4a598f1 100644 --- a/spec/utils/content_sanitizer_spec.rb +++ b/spec/utils/content_sanitizer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "utils/content_sanitizer" diff --git a/spec/utils/feed_discovery_spec.rb b/spec/utils/feed_discovery_spec.rb index ccb475948..4266d3ec1 100644 --- a/spec/utils/feed_discovery_spec.rb +++ b/spec/utils/feed_discovery_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "utils/feed_discovery" diff --git a/spec/utils/i18n_support_spec.rb b/spec/utils/i18n_support_spec.rb index e60e77b2d..65bdd3196 100644 --- a/spec/utils/i18n_support_spec.rb +++ b/spec/utils/i18n_support_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" describe "i18n" do diff --git a/spec/utils/opml_parser_spec.rb b/spec/utils/opml_parser_spec.rb index ea844b3e8..0939a0267 100644 --- a/spec/utils/opml_parser_spec.rb +++ b/spec/utils/opml_parser_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" app_require "utils/opml_parser" From 6da0bcc96b58f457ea49614aaa7333a0ccb4f543 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 29 Dec 2022 16:06:57 -0800 Subject: [PATCH 0446/1107] RuboCop: add parens for controller specs (#752) --- .rubocop_todo.yml | 8 -------- spec/controllers/debug_controller_spec.rb | 6 +++--- spec/controllers/exports_controller_spec.rb | 4 ++-- spec/controllers/feeds_controller_spec.rb | 4 ++-- spec/controllers/first_run_controller_spec.rb | 16 ++++++++-------- spec/controllers/imports_controller_spec.rb | 4 ++-- spec/controllers/sessions_controller_spec.rb | 12 ++++++------ spec/controllers/stories_controller_spec.rb | 16 ++++++++-------- spec/javascript/test_controller.rb | 6 +++--- 9 files changed, 34 insertions(+), 42 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6c1b22b50..f3b4539f9 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -656,17 +656,9 @@ Style/MethodCallWithArgsParentheses: - 'spec/commands/find_new_stories_spec.rb' - 'spec/commands/users/change_user_password_spec.rb' - 'spec/commands/users/sign_in_user_spec.rb' - - 'spec/controllers/debug_controller_spec.rb' - - 'spec/controllers/exports_controller_spec.rb' - - 'spec/controllers/feeds_controller_spec.rb' - - 'spec/controllers/first_run_controller_spec.rb' - - 'spec/controllers/imports_controller_spec.rb' - - 'spec/controllers/sessions_controller_spec.rb' - - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api_spec.rb' - 'spec/helpers/url_helpers_spec.rb' - 'spec/integration/feed_importing_spec.rb' - - 'spec/javascript/test_controller.rb' - 'spec/models/story_spec.rb' - 'spec/repositories/feed_repository_spec.rb' - 'spec/repositories/story_repository_spec.rb' diff --git a/spec/controllers/debug_controller_spec.rb b/spec/controllers/debug_controller_spec.rb index f0c7dd4b7..dc8a2fb62 100644 --- a/spec/controllers/debug_controller_spec.rb +++ b/spec/controllers/debug_controller_spec.rb @@ -8,15 +8,15 @@ describe DebugController do describe "GET /debug" do before do - delayed_job = double "Delayed::Job" + delayed_job = double("Delayed::Job") allow(delayed_job).to receive(:count).and_return(42) stub_const("Delayed::Job", delayed_job) - migration_status_instance = double "migration_status_instance" + migration_status_instance = double("migration_status_instance") allow(migration_status_instance) .to receive(:pending_migrations) .and_return(["Migration B - 2", "Migration C - 3"]) - migration_status = double "MigrationStatus" + migration_status = double("MigrationStatus") allow(migration_status) .to receive(:new).and_return(migration_status_instance) stub_const("MigrationStatus", migration_status) diff --git a/spec/controllers/exports_controller_spec.rb b/spec/controllers/exports_controller_spec.rb index 07285e56f..7e2d2efeb 100644 --- a/spec/controllers/exports_controller_spec.rb +++ b/spec/controllers/exports_controller_spec.rb @@ -19,7 +19,7 @@ def mock_export get "/feeds/export" - expect(last_response.body).to eq some_xml + expect(last_response.body).to eq(some_xml) end it "responds with xml content type" do @@ -27,7 +27,7 @@ def mock_export get "/feeds/export" - expect(last_response.header["Content-Type"]).to include "application/xml" + expect(last_response.header["Content-Type"]).to include("application/xml") end it "responds with disposition attachment" do diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index bbc28e605..32b715b10 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -110,8 +110,8 @@ def params(feed, **overrides) post("/feeds", feed_url:) - expect(last_response.status).to be 302 - expect(URI.parse(last_response.location).path).to eq "/" + expect(last_response.status).to be(302) + expect(URI.parse(last_response.location).path).to eq("/") end end diff --git a/spec/controllers/first_run_controller_spec.rb b/spec/controllers/first_run_controller_spec.rb index 14c8e62d7..418132b54 100644 --- a/spec/controllers/first_run_controller_spec.rb +++ b/spec/controllers/first_run_controller_spec.rb @@ -44,8 +44,8 @@ post "/setup/password", password: "foo", password_confirmation: "foo" - expect(last_response.status).to be 302 - expect(URI.parse(last_response.location).path).to eq "/feeds/import" + expect(last_response.status).to be(302) + expect(URI.parse(last_response.location).path).to eq("/feeds/import") end end @@ -81,16 +81,16 @@ session = { "rack.session" => { user_id: user.id } } get "/", {}, session - expect(last_response.status).to be 302 - expect(URI.parse(last_response.location).path).to eq "/news" + expect(last_response.status).to be(302) + expect(URI.parse(last_response.location).path).to eq("/news") get "/setup/password", {}, session - expect(last_response.status).to be 302 - expect(URI.parse(last_response.location).path).to eq "/news" + expect(last_response.status).to be(302) + expect(URI.parse(last_response.location).path).to eq("/news") get "/setup/tutorial", {}, session - expect(last_response.status).to be 302 - expect(URI.parse(last_response.location).path).to eq "/news" + expect(last_response.status).to be(302) + expect(URI.parse(last_response.location).path).to eq("/news") end end end diff --git a/spec/controllers/imports_controller_spec.rb b/spec/controllers/imports_controller_spec.rb index 2b052246e..d2ea96841 100644 --- a/spec/controllers/imports_controller_spec.rb +++ b/spec/controllers/imports_controller_spec.rb @@ -28,8 +28,8 @@ post "/feeds/import", "opml_file" => opml_file - expect(last_response.status).to be 302 - expect(URI.parse(last_response.location).path).to eq "/setup/tutorial" + expect(last_response.status).to be(302) + expect(URI.parse(last_response.location).path).to eq("/setup/tutorial") end end end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 93216b5a7..bfa5eef35 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -30,10 +30,10 @@ post "/login", password: "the-password" - expect(session[:user_id]).to eq 1 + expect(session[:user_id]).to eq(1) - expect(last_response.status).to be 302 - expect(URI.parse(last_response.location).path).to eq "/" + expect(last_response.status).to be(302) + expect(URI.parse(last_response.location).path).to eq("/") end it "redirects to the previous path when present" do @@ -43,7 +43,7 @@ post "/login", params, "rack.session" => { redirect_to: "/archive" } expect(session[:redirect_to]).to be_nil - expect(URI.parse(last_response.location).path).to eq "/archive" + expect(URI.parse(last_response.location).path).to eq("/archive") end end @@ -53,8 +53,8 @@ expect(session[:user_id]).to be_nil - expect(last_response.status).to be 302 - expect(URI.parse(last_response.location).path).to eq "/" + expect(last_response.status).to be(302) + expect(URI.parse(last_response.location).path).to eq("/") end end end diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index c06ba3702..758d4a6a2 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -100,7 +100,7 @@ put "/stories/#{story_one.id}", { is_read: true }.to_json - expect(story_one.is_read).to be true + expect(story_one.is_read).to be(true) end end @@ -110,7 +110,7 @@ put "/stories/#{story_one.id}", { is_read: "malformed" }.to_json - expect(story_one.is_read).to be true + expect(story_one.is_read).to be(true) end end end @@ -120,7 +120,7 @@ it "marks a story as permanently unread" do put "/stories/#{story_one.id}", { keep_unread: false }.to_json - expect(story_one.keep_unread).to be false + expect(story_one.keep_unread).to be(false) end end @@ -128,7 +128,7 @@ it "marks a story as permanently unread" do put "/stories/#{story_one.id}", { keep_unread: "malformed" }.to_json - expect(story_one.keep_unread).to be true + expect(story_one.keep_unread).to be(true) end end end @@ -138,7 +138,7 @@ it "marks a story as permanently starred" do put "/stories/#{story_one.id}", { is_starred: true }.to_json - expect(story_one.is_starred).to be true + expect(story_one.is_starred).to be(true) end end @@ -146,7 +146,7 @@ it "marks a story as permanently starred" do put "/stories/#{story_one.id}", { is_starred: "malformed" }.to_json - expect(story_one.is_starred).to be true + expect(story_one.is_starred).to be(true) end end end @@ -158,8 +158,8 @@ post "/stories/mark_all_as_read", story_ids: ["1", "2", "3"] - expect(last_response.status).to be 302 - expect(URI.parse(last_response.location).path).to eq "/news" + expect(last_response.status).to be(302) + expect(URI.parse(last_response.location).path).to eq("/news") end end diff --git a/spec/javascript/test_controller.rb b/spec/javascript/test_controller.rb index 470757949..3b1a7358d 100644 --- a/spec/javascript/test_controller.rb +++ b/spec/javascript/test_controller.rb @@ -33,16 +33,16 @@ def vendor_js_files "chai-backbone.js", "sinon-chai.js" ].map do |name| - File.join "vendor", "js", name + File.join("vendor", "js", name) end end def vendor_css_files - ["mocha.css"].map { |name| File.join "vendor", "css", name } + ["mocha.css"].map { |name| File.join("vendor", "css", name) } end def js_helper_files - ["spec_helper.js"].map { |name| File.join "spec", name } + ["spec_helper.js"].map { |name| File.join("spec", name) } end def js_lib_files From dd335211dd230fd08744545ab05504c85522e5e0 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 29 Dec 2022 16:28:06 -0800 Subject: [PATCH 0447/1107] RuboCop: fix lints for ExportsController (#753) --- .rubocop_todo.yml | 3 --- spec/controllers/exports_controller_spec.rb | 24 ++++++++++----------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f3b4539f9..531cfbe43 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -61,7 +61,6 @@ Naming/PredicateName: # Offense count: 5 RSpec/AnyInstance: Exclude: - - 'spec/controllers/exports_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/tasks/fetch_feed_spec.rb' @@ -147,7 +146,6 @@ RSpec/EmptyLineAfterFinalLet: Exclude: - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/commands/users/complete_setup_spec.rb' - - 'spec/controllers/exports_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/models/story_spec.rb' - 'spec/repositories/story_repository_spec.rb' @@ -259,7 +257,6 @@ RSpec/MessageExpectation: - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/commands/find_new_stories_spec.rb' - 'spec/controllers/debug_controller_spec.rb' - - 'spec/controllers/exports_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' diff --git a/spec/controllers/exports_controller_spec.rb b/spec/controllers/exports_controller_spec.rb index 7e2d2efeb..66be986e9 100644 --- a/spec/controllers/exports_controller_spec.rb +++ b/spec/controllers/exports_controller_spec.rb @@ -6,33 +6,31 @@ describe ExportsController do describe "GET /feeds/export" do - let(:some_xml) { "some dummy opml" } - before { allow(Feed).to receive(:all) } - - def mock_export - expect_any_instance_of(ExportToOpml) - .to receive(:to_xml).and_return(some_xml) + def expected_xml + <<~XML + + + + Feeds from Stringer + + + + XML end it "returns an OPML file" do - mock_export - get "/feeds/export" - expect(last_response.body).to eq(some_xml) + expect(last_response.body).to eq(expected_xml) end it "responds with xml content type" do - mock_export - get "/feeds/export" expect(last_response.header["Content-Type"]).to include("application/xml") end it "responds with disposition attachment" do - mock_export - get "/feeds/export" expected = From a6b168906880adf751e01143f64b44ba433007e2 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 29 Dec 2022 16:38:04 -0800 Subject: [PATCH 0448/1107] RuboCop: fix VerifiedDouble offenses in controller specs (#754) --- .rubocop_todo.yml | 4 ---- spec/controllers/debug_controller_spec.rb | 10 +++------- spec/controllers/feeds_controller_spec.rb | 4 ++-- spec/controllers/first_run_controller_spec.rb | 7 ++++--- spec/controllers/sessions_controller_spec.rb | 10 +++++----- 5 files changed, 14 insertions(+), 21 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 531cfbe43..883752c0c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -387,10 +387,6 @@ RSpec/VerifiedDoubles: - 'spec/commands/stories/mark_all_as_read_spec.rb' - 'spec/commands/stories/mark_feed_as_read_spec.rb' - 'spec/commands/users/sign_in_user_spec.rb' - - 'spec/controllers/debug_controller_spec.rb' - - 'spec/controllers/feeds_controller_spec.rb' - - 'spec/controllers/first_run_controller_spec.rb' - - 'spec/controllers/sessions_controller_spec.rb' - 'spec/fever_api/authentication_spec.rb' - 'spec/fever_api/read_feeds_groups_spec.rb' - 'spec/fever_api/read_feeds_spec.rb' diff --git a/spec/controllers/debug_controller_spec.rb b/spec/controllers/debug_controller_spec.rb index dc8a2fb62..f325f0936 100644 --- a/spec/controllers/debug_controller_spec.rb +++ b/spec/controllers/debug_controller_spec.rb @@ -8,18 +8,14 @@ describe DebugController do describe "GET /debug" do before do - delayed_job = double("Delayed::Job") - allow(delayed_job).to receive(:count).and_return(42) - stub_const("Delayed::Job", delayed_job) + allow(Delayed::Job).to receive(:count).and_return(42) - migration_status_instance = double("migration_status_instance") + migration_status_instance = instance_double(MigrationStatus) allow(migration_status_instance) .to receive(:pending_migrations) .and_return(["Migration B - 2", "Migration C - 3"]) - migration_status = double("MigrationStatus") - allow(migration_status) + allow(MigrationStatus) .to receive(:new).and_return(migration_status_instance) - stub_const("MigrationStatus", migration_status) end it "displays the current Ruby version" do diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index 32b715b10..6ac77e2ae 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -102,7 +102,7 @@ def params(feed, **overrides) describe "POST /feeds" do context "when the feed url is valid" do let(:feed_url) { "http://example.com/" } - let(:feed) { double(valid?: true) } + let(:feed) { instance_double(Feed, valid?: true) } it "adds the feed and queues it to be fetched" do expect(AddNewFeed).to receive(:add).with(feed_url).and_return(feed) @@ -130,7 +130,7 @@ def params(feed, **overrides) context "when the feed url is one we already subscribe to" do let(:feed_url) { "http://example.com/" } - let(:invalid_feed) { double(valid?: false) } + let(:invalid_feed) { instance_double(Feed, valid?: false) } it "adds the feed and queues it to be fetched" do expect(AddNewFeed) diff --git a/spec/controllers/first_run_controller_spec.rb b/spec/controllers/first_run_controller_spec.rb index 418132b54..7afc00764 100644 --- a/spec/controllers/first_run_controller_spec.rb +++ b/spec/controllers/first_run_controller_spec.rb @@ -39,8 +39,9 @@ end it "accepts confirmed passwords and redirects to next step" do + user = instance_double(User, id: 1) expect_any_instance_of(CreateUser) - .to receive(:create).with("foo").and_return(double(id: 1)) + .to receive(:create).with("foo").and_return(user) post "/setup/password", password: "foo", password_confirmation: "foo" @@ -50,8 +51,8 @@ end describe "GET /setup/tutorial" do - let(:user) { double } - let(:feeds) { [double, double] } + let(:user) { instance_double(User) } + let(:feeds) { [instance_double(Feed), instance_double(Feed)] } before do allow(UserRepository).to receive(:fetch).and_return(user) diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index bfa5eef35..589009797 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -26,20 +26,20 @@ end it "allows access when password is correct" do - allow(SignInUser).to receive(:sign_in).and_return(double(id: 1)) + user = create(:user) - post "/login", password: "the-password" + post "/login", password: user.password - expect(session[:user_id]).to eq(1) + expect(session[:user_id]).to eq(user.id) expect(last_response.status).to be(302) expect(URI.parse(last_response.location).path).to eq("/") end it "redirects to the previous path when present" do - allow(SignInUser).to receive(:sign_in).and_return(double(id: 1)) + user = create(:user) - params = { password: "the-password" } + params = { password: user.password } post "/login", params, "rack.session" => { redirect_to: "/archive" } expect(session[:redirect_to]).to be_nil From 18a5e8628523513582890726c941b6a811d7e5a7 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 29 Dec 2022 16:57:46 -0800 Subject: [PATCH 0449/1107] RuboCop: fix SaveBang offenses in controllers (#755) --- .rubocop_todo.yml | 2 -- app/commands/users/create_user.rb | 2 +- app/controllers/sinatra/first_run_controller.rb | 2 +- app/controllers/sinatra/stories_controller.rb | 2 +- app/repositories/story_repository.rb | 4 ---- spec/commands/users/create_user_spec.rb | 2 +- spec/controllers/first_run_controller_spec.rb | 2 +- spec/controllers/stories_controller_spec.rb | 4 ++-- 8 files changed, 7 insertions(+), 13 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 883752c0c..874e186f0 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -488,8 +488,6 @@ Rails/SaveBang: - 'app/commands/users/change_user_password.rb' - 'app/commands/users/complete_setup.rb' - 'app/commands/users/create_user.rb' - - 'app/controllers/sinatra/first_run_controller.rb' - - 'app/controllers/sinatra/stories_controller.rb' - 'app/repositories/feed_repository.rb' - 'app/repositories/story_repository.rb' - 'app/repositories/user_repository.rb' diff --git a/app/commands/users/create_user.rb b/app/commands/users/create_user.rb index b51d6eddb..46db7e891 100644 --- a/app/commands/users/create_user.rb +++ b/app/commands/users/create_user.rb @@ -7,7 +7,7 @@ def initialize(repository = User) @repo = repository end - def create(password) + def call(password) @repo.delete_all @repo.create( password: password, diff --git a/app/controllers/sinatra/first_run_controller.rb b/app/controllers/sinatra/first_run_controller.rb index 78e5bf5df..37f6f19d3 100644 --- a/app/controllers/sinatra/first_run_controller.rb +++ b/app/controllers/sinatra/first_run_controller.rb @@ -22,7 +22,7 @@ class Stringer < Sinatra::Base flash.now[:error] = t("first_run.password.flash.passwords_dont_match") erb :"first_run/password" else - user = CreateUser.new.create(params[:password]) + user = CreateUser.new.call(params[:password]) session[:user_id] = user.id redirect to("/feeds/import") diff --git a/app/controllers/sinatra/stories_controller.rb b/app/controllers/sinatra/stories_controller.rb index 9614f86d1..064e97709 100644 --- a/app/controllers/sinatra/stories_controller.rb +++ b/app/controllers/sinatra/stories_controller.rb @@ -39,7 +39,7 @@ class Stringer < Sinatra::Base story.keep_unread = !!json_params[:keep_unread] story.is_starred = !!json_params[:is_starred] - StoryRepository.save(story) + story.save! end post "/stories/mark_all_as_read" do diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index 606ba003a..a2c1032c0 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -48,10 +48,6 @@ def self.fetch_unread_for_feed_by_timestamp(feed_id, timestamp) .where("created_at < ? AND is_read = ?", timestamp, false) end - def self.save(story) - story.save - end - def self.exists?(id, feed_id) Story.exists?(entry_id: id, feed_id: feed_id) end diff --git a/spec/commands/users/create_user_spec.rb b/spec/commands/users/create_user_spec.rb index 6c93c7f4f..82fee8f90 100644 --- a/spec/commands/users/create_user_spec.rb +++ b/spec/commands/users/create_user_spec.rb @@ -14,7 +14,7 @@ expect(repo).to receive(:create) expect(repo).to receive(:delete_all) - command.create("password") + command.call("password") end end end diff --git a/spec/controllers/first_run_controller_spec.rb b/spec/controllers/first_run_controller_spec.rb index 7afc00764..934e4c113 100644 --- a/spec/controllers/first_run_controller_spec.rb +++ b/spec/controllers/first_run_controller_spec.rb @@ -41,7 +41,7 @@ it "accepts confirmed passwords and redirects to next step" do user = instance_double(User, id: 1) expect_any_instance_of(CreateUser) - .to receive(:create).with("foo").and_return(user) + .to receive(:call).with("foo").and_return(user) post "/setup/password", password: "foo", password_confirmation: "foo" diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index 758d4a6a2..39bd868b1 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -96,7 +96,7 @@ context "is_read parameter" do context "when it is not malformed" do it "marks a story as read" do - expect(StoryRepository).to receive(:save).once + expect(story_one).to receive(:save!).once put "/stories/#{story_one.id}", { is_read: true }.to_json @@ -106,7 +106,7 @@ context "when it is malformed" do it "marks a story as read" do - expect(StoryRepository).to receive(:save).once + expect(story_one).to receive(:save!).once put "/stories/#{story_one.id}", { is_read: "malformed" }.to_json From 740988bd0c6c1b9ccbe1cf91a0cb36ef12860763 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 29 Dec 2022 23:11:44 -0800 Subject: [PATCH 0450/1107] Create CODE_OF_CONDUCT.md (#758) --- CODE_OF_CONDUCT.md | 128 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..fa581a793 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +robert@boon.gl. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. From 3d4dbe0e31856453b0bc57858b7580e3274a8d9b Mon Sep 17 00:00:00 2001 From: ilakast Date: Sat, 31 Dec 2022 20:25:09 +0000 Subject: [PATCH 0451/1107] Update README.md (#760) updated info about Heroku plans not being free anymore --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 5f5b44196..f4dc1f2fc 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Stringer is a Ruby app based on Sinatra, ActiveRecord, PostgreSQL, Backbone.js a [![Deploy to Heroku](https://cdn.herokuapp.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/stringer-rss/stringer) -Stringer will run just fine on the Heroku free plan. +Stringer will run just fine on the Eco/Basic Heroku plans. Instructions are provided for deploying to [Heroku manually](/docs/Heroku.md), to any Ruby compatible [Linux-based VPS](/docs/VPS.md), to [Docker](docs/docker.md) and to [OpenShift](/docs/OpenShift.md). @@ -79,9 +79,6 @@ If you would like to translate Stringer to your preferred language, please use [ ### Clean up old read stories on Heroku -If you are on the Heroku free plan, there is a 10k row limit so you will -eventually run out of space. - You can clean up old stories by running: `rake cleanup_old_stories` By default, this removes read stories that are more than 30 days old (that From 460ee12983abfb8f1d602cecf43f212f2455a4ad Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 31 Dec 2022 15:35:18 -0800 Subject: [PATCH 0452/1107] RuboCop: fix AnyInstance offenses in controllers (#757) --- .rubocop_todo.yml | 2 -- app/commands/stories/mark_all_as_read.rb | 6 +++++- app/commands/users/create_user.rb | 4 ++++ app/controllers/sinatra/first_run_controller.rb | 2 +- app/controllers/sinatra/stories_controller.rb | 2 +- spec/commands/stories/mark_all_as_read_spec.rb | 6 +++--- spec/commands/users/create_user_spec.rb | 12 ++++-------- spec/controllers/first_run_controller_spec.rb | 3 +-- spec/controllers/stories_controller_spec.rb | 2 +- 9 files changed, 20 insertions(+), 19 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 874e186f0..6353fdab8 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -61,8 +61,6 @@ Naming/PredicateName: # Offense count: 5 RSpec/AnyInstance: Exclude: - - 'spec/controllers/first_run_controller_spec.rb' - - 'spec/controllers/stories_controller_spec.rb' - 'spec/tasks/fetch_feed_spec.rb' # Offense count: 4 diff --git a/app/commands/stories/mark_all_as_read.rb b/app/commands/stories/mark_all_as_read.rb index a74dceaf9..c13d3aa76 100644 --- a/app/commands/stories/mark_all_as_read.rb +++ b/app/commands/stories/mark_all_as_read.rb @@ -8,7 +8,11 @@ def initialize(story_ids, repository = StoryRepository) @repo = repository end - def mark_as_read + def self.call(*args) + new(*args).call + end + + def call @repo.fetch_by_ids(@story_ids).update_all(is_read: true) end end diff --git a/app/commands/users/create_user.rb b/app/commands/users/create_user.rb index 46db7e891..955d615de 100644 --- a/app/commands/users/create_user.rb +++ b/app/commands/users/create_user.rb @@ -7,6 +7,10 @@ def initialize(repository = User) @repo = repository end + def self.call(password) + new.call(password) + end + def call(password) @repo.delete_all @repo.create( diff --git a/app/controllers/sinatra/first_run_controller.rb b/app/controllers/sinatra/first_run_controller.rb index 37f6f19d3..8556addc6 100644 --- a/app/controllers/sinatra/first_run_controller.rb +++ b/app/controllers/sinatra/first_run_controller.rb @@ -22,7 +22,7 @@ class Stringer < Sinatra::Base flash.now[:error] = t("first_run.password.flash.passwords_dont_match") erb :"first_run/password" else - user = CreateUser.new.call(params[:password]) + user = CreateUser.call(params[:password]) session[:user_id] = user.id redirect to("/feeds/import") diff --git a/app/controllers/sinatra/stories_controller.rb b/app/controllers/sinatra/stories_controller.rb index 064e97709..c36ce0573 100644 --- a/app/controllers/sinatra/stories_controller.rb +++ b/app/controllers/sinatra/stories_controller.rb @@ -43,7 +43,7 @@ class Stringer < Sinatra::Base end post "/stories/mark_all_as_read" do - MarkAllAsRead.new(params[:story_ids]).mark_as_read + MarkAllAsRead.call(params[:story_ids]) redirect to("/news") end diff --git a/spec/commands/stories/mark_all_as_read_spec.rb b/spec/commands/stories/mark_all_as_read_spec.rb index 656302f9c..b9880759e 100644 --- a/spec/commands/stories/mark_all_as_read_spec.rb +++ b/spec/commands/stories/mark_all_as_read_spec.rb @@ -5,14 +5,14 @@ app_require "commands/stories/mark_all_as_read" describe MarkAllAsRead do - describe "#mark_as_read" do + describe "#call" do let(:stories) { double } let(:repo) { double(fetch_by_ids: stories) } it "marks all stories as read" do - command = MarkAllAsRead.new([1, 2], repo) expect(stories).to receive(:update_all).with(is_read: true) - command.mark_as_read + + MarkAllAsRead.call([1, 2], repo) end end end diff --git a/spec/commands/users/create_user_spec.rb b/spec/commands/users/create_user_spec.rb index 82fee8f90..c8a2fde8e 100644 --- a/spec/commands/users/create_user_spec.rb +++ b/spec/commands/users/create_user_spec.rb @@ -5,16 +5,12 @@ app_require "commands/users/create_user" describe CreateUser do - let(:repo) { double } - - describe "#create" do + describe "#call" do it "removes existing users and create a user with the password supplied" do - command = CreateUser.new(repo) - - expect(repo).to receive(:create) - expect(repo).to receive(:delete_all) + expect(User).to receive(:create) + expect(User).to receive(:delete_all) - command.call("password") + described_class.call("password") end end end diff --git a/spec/controllers/first_run_controller_spec.rb b/spec/controllers/first_run_controller_spec.rb index 934e4c113..454b76011 100644 --- a/spec/controllers/first_run_controller_spec.rb +++ b/spec/controllers/first_run_controller_spec.rb @@ -40,8 +40,7 @@ it "accepts confirmed passwords and redirects to next step" do user = instance_double(User, id: 1) - expect_any_instance_of(CreateUser) - .to receive(:call).with("foo").and_return(user) + expect(CreateUser).to receive(:call).with("foo").and_return(user) post "/setup/password", password: "foo", password_confirmation: "foo" diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index 39bd868b1..2446daa10 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -154,7 +154,7 @@ describe "POST /stories/mark_all_as_read" do it "marks all unread stories as read and reload the page" do - expect_any_instance_of(MarkAllAsRead).to receive(:mark_as_read).once + expect(MarkAllAsRead).to receive(:call).once post "/stories/mark_all_as_read", story_ids: ["1", "2", "3"] From ab10806a26d45c50df0984a41bb9a0a26bc99ec6 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 31 Dec 2022 16:03:45 -0800 Subject: [PATCH 0453/1107] Dev: remove racksh (#761) It currently [doesn't work with Ruby 3.2][r3]. For our purposes, we can instead replace it with a small rake task. [r3]: https://github.com/sickill/racksh/pull/15 --- Gemfile | 1 - Gemfile.lock | 4 ---- Procfile | 1 - README.md | 2 +- Rakefile | 5 +++++ docs/VPS.md | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 4153f7c03..c08b3cea2 100644 --- a/Gemfile +++ b/Gemfile @@ -14,7 +14,6 @@ gem "feedjira" gem "httparty" gem "pg" gem "puma", "~> 6.0" -gem "racksh" gem "rack-ssl" gem "sass" gem "sinatra" diff --git a/Gemfile.lock b/Gemfile.lock index 4c244265e..1f48fefc0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -175,9 +175,6 @@ GEM rack rack-test (2.0.2) rack (>= 1.3) - racksh (1.0.0) - rack (>= 1.0) - rack-test (>= 0.5) rails (7.0.4) actioncable (= 7.0.4) actionmailbox (= 7.0.4) @@ -333,7 +330,6 @@ DEPENDENCIES pry-byebug puma (~> 6.0) rack-ssl - racksh rails (~> 7.0.1) rspec rspec-html-matchers diff --git a/Procfile b/Procfile index cceb33ddf..4a7ea7b92 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1 @@ web: bundle exec puma -p $PORT -C ./config/puma.rb -console: bundle exec racksh diff --git a/README.md b/README.md index f4dc1f2fc..9818718ff 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ foreman start The application will be running on port `5000`. -You can launch an interactive console (a la `rails c`) using `racksh`. +You can launch an interactive console (a la `rails c`) using `rake console`. ## Acknowledgments diff --git a/Rakefile b/Rakefile index 4bc059263..87fc524b4 100644 --- a/Rakefile +++ b/Rakefile @@ -19,6 +19,11 @@ require_relative "./app/tasks/fetch_feeds" require_relative "./app/tasks/change_password" require_relative "./app/tasks/remove_old_stories" +desc "Open an irb session preloaded with the app" +task :console do + sh "irb -r ./app.rb" +end + desc "Fetch all feeds." task :fetch_feeds do FetchFeeds.new(Feed.all).fetch_all diff --git a/docs/VPS.md b/docs/VPS.md index 9c05452d2..51b61d3b2 100644 --- a/docs/VPS.md +++ b/docs/VPS.md @@ -130,7 +130,7 @@ Logout stringer user, install systemd services: As stringer user, close existing Stringer instance: - exit # exit racksh and app + exit # exit app Start app as a systemd service and make app run at startup From 91d35a3826068d1d4775d53cabfe84bdd18b214d Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 31 Dec 2022 16:09:05 -0800 Subject: [PATCH 0454/1107] RuboCop: fix NestedGroups offenses in stories controller spec (#762) --- .rubocop_todo.yml | 1 - spec/controllers/stories_controller_spec.rb | 52 ++++++++------------- 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6353fdab8..8e1c263ac 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -343,7 +343,6 @@ RSpec/NamedSubject: RSpec/NestedGroups: Exclude: - 'spec/commands/feeds/add_new_feed_spec.rb' - - 'spec/controllers/stories_controller_spec.rb' - 'spec/integration/feed_importing_spec.rb' # Offense count: 2 diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index 2446daa10..b261c5d74 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -94,60 +94,48 @@ describe "PUT /stories/:id" do before { allow(StoryRepository).to receive(:fetch).and_return(story_one) } context "is_read parameter" do - context "when it is not malformed" do - it "marks a story as read" do - expect(story_one).to receive(:save!).once + it "marks a story as read when it is not malformed" do + expect(story_one).to receive(:save!).once - put "/stories/#{story_one.id}", { is_read: true }.to_json + put "/stories/#{story_one.id}", { is_read: true }.to_json - expect(story_one.is_read).to be(true) - end + expect(story_one.is_read).to be(true) end - context "when it is malformed" do - it "marks a story as read" do - expect(story_one).to receive(:save!).once + it "marks a story as read when it is malformed" do + expect(story_one).to receive(:save!).once - put "/stories/#{story_one.id}", { is_read: "malformed" }.to_json + put "/stories/#{story_one.id}", { is_read: "malformed" }.to_json - expect(story_one.is_read).to be(true) - end + expect(story_one.is_read).to be(true) end end context "keep_unread parameter" do - context "when it is not malformed" do - it "marks a story as permanently unread" do - put "/stories/#{story_one.id}", { keep_unread: false }.to_json + it "marks a story as permanently unread when it is not malformed" do + put "/stories/#{story_one.id}", { keep_unread: false }.to_json - expect(story_one.keep_unread).to be(false) - end + expect(story_one.keep_unread).to be(false) end - context "when it is malformed" do - it "marks a story as permanently unread" do - put "/stories/#{story_one.id}", { keep_unread: "malformed" }.to_json + it "marks a story as permanently unread when it is malformed" do + put "/stories/#{story_one.id}", { keep_unread: "malformed" }.to_json - expect(story_one.keep_unread).to be(true) - end + expect(story_one.keep_unread).to be(true) end end context "is_starred parameter" do - context "when it is not malformed" do - it "marks a story as permanently starred" do - put "/stories/#{story_one.id}", { is_starred: true }.to_json + it "marks a story as permanently starred when it is not malformed" do + put "/stories/#{story_one.id}", { is_starred: true }.to_json - expect(story_one.is_starred).to be(true) - end + expect(story_one.is_starred).to be(true) end - context "when it is malformed" do - it "marks a story as permanently starred" do - put "/stories/#{story_one.id}", { is_starred: "malformed" }.to_json + it "marks a story as permanently starred when it is malformed" do + put "/stories/#{story_one.id}", { is_starred: "malformed" }.to_json - expect(story_one.is_starred).to be(true) - end + expect(story_one.is_starred).to be(true) end end end From 85aa590a8abe0d82d715d673052db3391fec9953 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Sat, 31 Dec 2022 16:09:13 -0800 Subject: [PATCH 0455/1107] Update Ruby to version 3.2.0 (#743) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- .circleci/config.yml | 2 +- .ruby-version | 2 +- .tool-versions | 2 +- Dockerfile | 2 +- Gemfile.lock | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6c8e2edd9..e79700049 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ jobs: build: parallelism: 1 docker: - - image: cimg/ruby:3.1.3-browsers + - image: cimg/ruby:3.2.0-browsers environment: BUNDLE_JOBS: 3 BUNDLE_RETRY: 3 diff --git a/.ruby-version b/.ruby-version index ff365e06b..944880fa1 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.1.3 +3.2.0 diff --git a/.tool-versions b/.tool-versions index ddf26c264..e57b760fc 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ -ruby 3.1.3 +ruby 3.2.0 bundler 2.3.25 postgres 14.6 diff --git a/Dockerfile b/Dockerfile index 307350dfb..7dde75564 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.1.3 +FROM ruby:3.2.0 ENV RACK_ENV=production ENV PORT=8080 diff --git a/Gemfile.lock b/Gemfile.lock index 1f48fefc0..6e7319853 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -353,7 +353,7 @@ DEPENDENCIES will_paginate RUBY VERSION - ruby 3.1.3 + ruby 3.2.0 BUNDLED WITH 2.3.25 From 22426adfe7929937ae5438041819a685a3e0366b Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 31 Dec 2022 16:23:38 -0800 Subject: [PATCH 0456/1107] Deps: update Nokogiri version (#764) The current release version [does not support Ruby 3.2][r3]. [r3]: https://github.com/sparklemotion/nokogiri/discussions/2747 --- Gemfile | 1 + Gemfile.lock | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index c08b3cea2..2f91f7809 100644 --- a/Gemfile +++ b/Gemfile @@ -12,6 +12,7 @@ gem "delayed_job_active_record" gem "feedbag" gem "feedjira" gem "httparty" +gem "nokogiri", "~> 1.14.0.rc1" gem "pg" gem "puma", "~> 6.0" gem "rack-ssl" diff --git a/Gemfile.lock b/Gemfile.lock index 6e7319853..5b64567b3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -151,7 +151,7 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) - nokogiri (1.13.10) + nokogiri (1.14.0.rc1) mini_portile2 (~> 2.8.0) racc (~> 1.4) parallel (1.22.1) @@ -326,6 +326,7 @@ DEPENDENCIES feedbag feedjira httparty + nokogiri (~> 1.14.0.rc1) pg pry-byebug puma (~> 6.0) From 6fa346c3c9b22158cd5372a63d14e1293194c3bb Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 31 Dec 2022 16:23:49 -0800 Subject: [PATCH 0457/1107] RuboCop: fix CollectionMethods offenses (#763) --- .rubocop_todo.yml | 9 --------- app/controllers/sinatra/stories_controller.rb | 2 +- app/fever_api/response.rb | 2 +- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8e1c263ac..f0b4eb1a2 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -543,15 +543,6 @@ Rails/WhereNot: Exclude: - 'spec/commands/feeds/import_from_opml_spec.rb' -# Offense count: 2 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: PreferredMethods, MethodsAcceptingSymbol. -# MethodsAcceptingSymbol: inject, reduce -Style/CollectionMethods: - Exclude: - - 'app/controllers/sinatra/stories_controller.rb' - - 'app/fever_api/response.rb' - # Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. diff --git a/app/controllers/sinatra/stories_controller.rb b/app/controllers/sinatra/stories_controller.rb index c36ce0573..d67ba7775 100644 --- a/app/controllers/sinatra/stories_controller.rb +++ b/app/controllers/sinatra/stories_controller.rb @@ -14,7 +14,7 @@ class Stringer < Sinatra::Base @feed = FeedRepository.fetch(params[:feed_id]) @stories = StoryRepository.feed(params[:feed_id]) - @unread_stories = @stories.find_all { |story| !story.is_read } + @unread_stories = @stories.reject(&:is_read) erb :feed end diff --git a/app/fever_api/response.rb b/app/fever_api/response.rb index 327c40f55..19c1e324a 100644 --- a/app/fever_api/response.rb +++ b/app/fever_api/response.rb @@ -45,7 +45,7 @@ def initialize(params) def to_json(*_args) base_response = { api_version: API_VERSION } ACTIONS - .inject(base_response) { |a, e| a.merge!(e.new.call(@params)) } + .reduce(base_response) { |a, e| a.merge!(e.new.call(@params)) } .to_json end end From fcc671c9bbd6ab8ff28dcf8baff7d001745bafb0 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 31 Dec 2022 16:28:10 -0800 Subject: [PATCH 0458/1107] RuboCop: fix DoubleNegation offense (#765) --- .rubocop_todo.yml | 8 -------- app/controllers/sinatra/stories_controller.rb | 6 +----- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f0b4eb1a2..b09ce42f4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -543,14 +543,6 @@ Rails/WhereNot: Exclude: - 'spec/commands/feeds/import_from_opml_spec.rb' -# Offense count: 3 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: allowed_in_returns, forbidden -Style/DoubleNegation: - Exclude: - - 'app/controllers/sinatra/stories_controller.rb' - # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedVars. diff --git a/app/controllers/sinatra/stories_controller.rb b/app/controllers/sinatra/stories_controller.rb index d67ba7775..29b5bc272 100644 --- a/app/controllers/sinatra/stories_controller.rb +++ b/app/controllers/sinatra/stories_controller.rb @@ -35,11 +35,7 @@ class Stringer < Sinatra::Base json_params = JSON.parse(request.body.read, symbolize_names: true) story = StoryRepository.fetch(params[:id]) - story.is_read = !!json_params[:is_read] - story.keep_unread = !!json_params[:keep_unread] - story.is_starred = !!json_params[:is_starred] - - story.save! + story.update!(json_params.slice(:is_read, :is_starred, :keep_unread)) end post "/stories/mark_all_as_read" do From 3ebfa9ec1ed6f8c37759ff37a006028072c83da0 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 31 Dec 2022 16:33:08 -0800 Subject: [PATCH 0459/1107] RuboCop: fix EmptyLine offenses (#766) --- .rubocop_todo.yml | 17 ----------------- spec/commands/feeds/add_new_feed_spec.rb | 1 + spec/commands/users/complete_setup_spec.rb | 1 + spec/controllers/stories_controller_spec.rb | 3 +++ spec/models/story_spec.rb | 1 + spec/repositories/story_repository_spec.rb | 1 + 6 files changed, 7 insertions(+), 17 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b09ce42f4..69a89a078 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -138,23 +138,6 @@ RSpec/DescribedClass: - 'spec/utils/feed_discovery_spec.rb' - 'spec/utils/opml_parser_spec.rb' -# Offense count: 7 -# This cop supports safe autocorrection (--autocorrect). -RSpec/EmptyLineAfterFinalLet: - Exclude: - - 'spec/commands/feeds/add_new_feed_spec.rb' - - 'spec/commands/users/complete_setup_spec.rb' - - 'spec/controllers/stories_controller_spec.rb' - - 'spec/models/story_spec.rb' - - 'spec/repositories/story_repository_spec.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowConsecutiveOneLiners. -RSpec/EmptyLineAfterHook: - Exclude: - - 'spec/controllers/stories_controller_spec.rb' - # Offense count: 63 # Configuration parameters: Max, CountAsOne. RSpec/ExampleLength: diff --git a/spec/commands/feeds/add_new_feed_spec.rb b/spec/commands/feeds/add_new_feed_spec.rb index e0eb8d39d..8893f5434 100644 --- a/spec/commands/feeds/add_new_feed_spec.rb +++ b/spec/commands/feeds/add_new_feed_spec.rb @@ -8,6 +8,7 @@ describe "#add" do context "feed cannot be discovered" do let(:discoverer) { double(discover: false) } + it "returns false if cant discover any feeds" do result = AddNewFeed.add("http://not-a-feed.com", discoverer) diff --git a/spec/commands/users/complete_setup_spec.rb b/spec/commands/users/complete_setup_spec.rb index 98fca9f1a..5c1ad2a88 100644 --- a/spec/commands/users/complete_setup_spec.rb +++ b/spec/commands/users/complete_setup_spec.rb @@ -6,6 +6,7 @@ describe CompleteSetup do let(:user) { build(:user) } + it "marks setup as complete" do expect(user).to receive(:save).once diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index b261c5d74..1801fee0f 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -65,6 +65,7 @@ let(:read_one) { build(:story, :read) } let(:read_two) { build(:story, :read) } let(:stories) { [read_one, read_two].paginate } + before { allow(StoryRepository).to receive(:read).and_return(stories) } it "displays the list of read stories with pagination" do @@ -80,6 +81,7 @@ let(:starred_one) { build(:story, :starred) } let(:starred_two) { build(:story, :starred) } let(:stories) { [starred_one, starred_two].paginate } + before { allow(StoryRepository).to receive(:starred).and_return(stories) } it "displays the list of starred stories with pagination" do @@ -93,6 +95,7 @@ describe "PUT /stories/:id" do before { allow(StoryRepository).to receive(:fetch).and_return(story_one) } + context "is_read parameter" do it "marks a story as read when it is not malformed" do expect(story_one).to receive(:save!).once diff --git a/spec/models/story_spec.rb b/spec/models/story_spec.rb index cc6729698..e015054c9 100644 --- a/spec/models/story_spec.rb +++ b/spec/models/story_spec.rb @@ -42,6 +42,7 @@ describe "#source" do let(:feed) { Feed.new(name: "Superfeed") } + before { story.feed = feed } it "returns the feeds name" do diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 98aa3fada..6b4abc5ad 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -8,6 +8,7 @@ describe StoryRepository do describe ".add" do let(:feed) { double(url: "http://blog.golang.org/feed.atom") } + before { allow(Story).to receive(:create) } it "normalizes story urls" do From 7cf31ffd9b60c9a03c71681eb92b3a0dbba7b102 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 31 Dec 2022 16:50:12 -0800 Subject: [PATCH 0460/1107] RuboCop: fix ExampleWording offenses (#767) --- .rubocop_todo.yml | 14 -------------- spec/commands/find_new_stories_spec.rb | 8 ++++---- spec/controllers/first_run_controller_spec.rb | 2 +- spec/controllers/stories_controller_spec.rb | 2 +- spec/tasks/fetch_feed_spec.rb | 8 ++++---- spec/tasks/remove_old_stories_spec.rb | 8 ++++---- spec/utils/i18n_support_spec.rb | 6 +++--- spec/utils/opml_parser_spec.rb | 2 +- 8 files changed, 18 insertions(+), 32 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 69a89a078..8e5048161 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -165,20 +165,6 @@ RSpec/ExampleLength: - 'spec/utils/feed_discovery_spec.rb' - 'spec/utils/opml_parser_spec.rb' -# Offense count: 18 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: CustomTransform, IgnoredWords, DisallowedExamples. -# DisallowedExamples: works -RSpec/ExampleWording: - Exclude: - - 'spec/commands/find_new_stories_spec.rb' - - 'spec/controllers/first_run_controller_spec.rb' - - 'spec/controllers/stories_controller_spec.rb' - - 'spec/tasks/fetch_feed_spec.rb' - - 'spec/tasks/remove_old_stories_spec.rb' - - 'spec/utils/i18n_support_spec.rb' - - 'spec/utils/opml_parser_spec.rb' - # Offense count: 2 RSpec/ExpectInHook: Exclude: diff --git a/spec/commands/find_new_stories_spec.rb b/spec/commands/find_new_stories_spec.rb index 8f4af7c98..5f2f45c63 100644 --- a/spec/commands/find_new_stories_spec.rb +++ b/spec/commands/find_new_stories_spec.rb @@ -10,7 +10,7 @@ context "the feed contains no new stories" do before { allow(StoryRepository).to receive(:exists?).and_return(true) } - it "should find zero new stories" do + it "finds zero new stories" do story1 = double(published: nil, id: "story1") story2 = double(published: nil, id: "story2") feed = double(entries: [story1, story2]) @@ -21,7 +21,7 @@ end context "the feed contains new stories" do - it "should return stories that are not found in the database" do + it "returns stories that are not found in the database" do story1 = double(published: nil, id: "story1") story2 = double(published: nil, id: "story2") feed = double(entries: [story1, story2]) @@ -36,7 +36,7 @@ end end - it "should scan until matching the last story id" do + it "scans until matching the last story id" do new_story = double(published: nil, id: "new-story") old_story = double(published: nil, id: "old-story") feed = double(last_modified: nil, entries: [new_story, old_story]) @@ -50,7 +50,7 @@ expect(result).to eq [new_story] end - it "should ignore stories older than 3 days" do + it "ignores stories older than 3 days" do new_stories = [ double(published: 1.hour.ago, id: "new-story"), double(published: 2.days.ago, id: "new-story") diff --git a/spec/controllers/first_run_controller_spec.rb b/spec/controllers/first_run_controller_spec.rb index 454b76011..b449ea83b 100644 --- a/spec/controllers/first_run_controller_spec.rb +++ b/spec/controllers/first_run_controller_spec.rb @@ -76,7 +76,7 @@ end context "when a user has been setup" do - it "should redirect any requests to first run stuff" do + it "redirects any requests to first run stuff" do user = create(:user, :setup_complete) session = { "rack.session" => { user_id: user.id } } diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index 1801fee0f..c4fdda94e 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -40,7 +40,7 @@ expect(last_response.body).to have_tag("#add-feed") end - it "should have correct footer links" do + it "has correct footer links" do get "/news" page = last_response.body diff --git a/spec/tasks/fetch_feed_spec.rb b/spec/tasks/fetch_feed_spec.rb index 0a07c6df0..457d80e0b 100644 --- a/spec/tasks/fetch_feed_spec.rb +++ b/spec/tasks/fetch_feed_spec.rb @@ -21,7 +21,7 @@ end context "when feed has not been modified" do - it "should not try to fetch posts" do + it "does not try to fetch posts" do client = class_spy(HTTParty) parser = class_double(Feedjira, parse: 304) @@ -53,7 +53,7 @@ end context "when no new posts have been added" do - it "should not add any new posts" do + it "does not add any new posts" do fake_feed = double(last_modified: Time.new(2012, 12, 31)) client = class_spy(HTTParty) parser = class_double(Feedjira, parse: fake_feed) @@ -83,7 +83,7 @@ .to receive(:new_stories).and_return([new_story]) end - it "should only add posts that are new" do + it "only adds posts that are new" do expect(StoryRepository).to receive(:add).with( new_story, daring_fireball @@ -98,7 +98,7 @@ ).fetch end - it "should update the last fetched time for the feed" do + it "updates the last fetched time for the feed" do expect(FeedRepository).to receive(:update_last_fetched) .with(daring_fireball, now) diff --git a/spec/tasks/remove_old_stories_spec.rb b/spec/tasks/remove_old_stories_spec.rb index 7680edd23..08a36df7f 100644 --- a/spec/tasks/remove_old_stories_spec.rb +++ b/spec/tasks/remove_old_stories_spec.rb @@ -11,7 +11,7 @@ stories end - it "should pass along the number of days to the story repository query" do + it "passes along the number of days to the story repository query" do allow(RemoveOldStories).to receive(:pruned_feeds) { [] } expect(StoryRepository).to receive(:unstarred_read_stories_older_than) @@ -20,7 +20,7 @@ RemoveOldStories.remove!(7) end - it "should request deletion of all old stories" do + it "requests deletion of all old stories" do allow(RemoveOldStories).to receive(:pruned_feeds) { [] } allow(StoryRepository) .to receive(:unstarred_read_stories_older_than) { stories_mock } @@ -30,7 +30,7 @@ RemoveOldStories.remove!(11) end - it "should fetch affected feeds by id" do + it "fetches affected feeds by id" do allow(RemoveOldStories).to receive(:old_stories) do stories = [double("story", feed_id: 3), double("story", feed_id: 5)] allow(stories).to receive(:delete_all) @@ -43,7 +43,7 @@ RemoveOldStories.remove!(13) end - it "should update last_fetched on affected feeds" do + it "updates last_fetched on affected feeds" do feeds = [double("feed a"), double("feed b")] allow(RemoveOldStories).to receive(:pruned_feeds) { feeds } allow(RemoveOldStories).to receive(:old_stories) { stories_mock } diff --git a/spec/utils/i18n_support_spec.rb b/spec/utils/i18n_support_spec.rb index 65bdd3196..d469036a7 100644 --- a/spec/utils/i18n_support_spec.rb +++ b/spec/utils/i18n_support_spec.rb @@ -12,7 +12,7 @@ context "when no locale was set" do let(:locale) { nil } - it "should load default locale" do + it "loads default locale" do expect(I18n.locale.to_s).to eq "en" expect(I18n.locale.to_s).not_to be_nil end @@ -21,7 +21,7 @@ context "when locale was set" do let(:locale) { "en" } - it "should load default locale" do + it "loads default locale" do expect(I18n.locale.to_s).to eq "en" expect(I18n.t("layout.title")).to eq "stringer | your rss buddy" end @@ -30,7 +30,7 @@ context "when a missing locale was set" do let(:locale) { "xx" } - it "should not find localization strings" do + it "does not find localization strings" do expect(I18n.t("layout.title", locale: ENV["LOCALE"].to_sym)) .not_to eq "stringer | your rss buddy" end diff --git a/spec/utils/opml_parser_spec.rb b/spec/utils/opml_parser_spec.rb index 0939a0267..afdf21fad 100644 --- a/spec/utils/opml_parser_spec.rb +++ b/spec/utils/opml_parser_spec.rb @@ -8,7 +8,7 @@ let(:parser) { OpmlParser.new } describe "#parse_feeds" do - it "it returns a hash of feed details from an OPML file" do + it "returns a hash of feed details from an OPML file" do result = parser.parse_feeds(<<-EOS) From ddbebcbe57491cd857d3849fa3f91b6ad273d4e3 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 31 Dec 2022 16:59:18 -0800 Subject: [PATCH 0461/1107] RuboCop: clean up StoriesController contexts (#768) --- .rubocop_todo.yml | 1 - spec/controllers/stories_controller_spec.rb | 52 +++++++++------------ 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8e5048161..f76e21a7b 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -82,7 +82,6 @@ RSpec/ContextWording: - 'spec/commands/feeds/import_from_opml_spec.rb' - 'spec/commands/find_new_stories_spec.rb' - 'spec/commands/stories/mark_group_as_read_spec.rb' - - 'spec/controllers/stories_controller_spec.rb' - 'spec/helpers/authentications_helper_spec.rb' - 'spec/integration/feed_importing_spec.rb' - 'spec/tasks/fetch_feed_spec.rb' diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index c4fdda94e..fb7e7af4b 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -96,50 +96,44 @@ describe "PUT /stories/:id" do before { allow(StoryRepository).to receive(:fetch).and_return(story_one) } - context "is_read parameter" do - it "marks a story as read when it is not malformed" do - expect(story_one).to receive(:save!).once + it "marks a story as read when it is_read not malformed" do + expect(story_one).to receive(:save!).once - put "/stories/#{story_one.id}", { is_read: true }.to_json + put "/stories/#{story_one.id}", { is_read: true }.to_json - expect(story_one.is_read).to be(true) - end + expect(story_one.is_read).to be(true) + end - it "marks a story as read when it is malformed" do - expect(story_one).to receive(:save!).once + it "marks a story as read when is_read is malformed" do + expect(story_one).to receive(:save!).once - put "/stories/#{story_one.id}", { is_read: "malformed" }.to_json + put "/stories/#{story_one.id}", { is_read: "malformed" }.to_json - expect(story_one.is_read).to be(true) - end + expect(story_one.is_read).to be(true) end - context "keep_unread parameter" do - it "marks a story as permanently unread when it is not malformed" do - put "/stories/#{story_one.id}", { keep_unread: false }.to_json + it "marks a story as keep unread when it keep_unread not malformed" do + put "/stories/#{story_one.id}", { keep_unread: false }.to_json - expect(story_one.keep_unread).to be(false) - end + expect(story_one.keep_unread).to be(false) + end - it "marks a story as permanently unread when it is malformed" do - put "/stories/#{story_one.id}", { keep_unread: "malformed" }.to_json + it "marks a story as keep unread when keep_unread is malformed" do + put "/stories/#{story_one.id}", { keep_unread: "malformed" }.to_json - expect(story_one.keep_unread).to be(true) - end + expect(story_one.keep_unread).to be(true) end - context "is_starred parameter" do - it "marks a story as permanently starred when it is not malformed" do - put "/stories/#{story_one.id}", { is_starred: true }.to_json + it "marks a story as starred when is_starred is not malformed" do + put "/stories/#{story_one.id}", { is_starred: true }.to_json - expect(story_one.is_starred).to be(true) - end + expect(story_one.is_starred).to be(true) + end - it "marks a story as permanently starred when it is malformed" do - put "/stories/#{story_one.id}", { is_starred: "malformed" }.to_json + it "marks a story as starred when is_starred is malformed" do + put "/stories/#{story_one.id}", { is_starred: "malformed" }.to_json - expect(story_one.is_starred).to be(true) - end + expect(story_one.is_starred).to be(true) end end From dd9dcf8928b0f673013c8185546c42c9f063eb9f Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 1 Jan 2023 21:40:46 -0800 Subject: [PATCH 0462/1107] RuboCop: shorten examples for FirstRunController (#769) --- .rubocop_todo.yml | 1 - spec/controllers/first_run_controller_spec.rb | 19 +++++++------------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f76e21a7b..bdf9e8f6c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -144,7 +144,6 @@ RSpec/ExampleLength: - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/commands/feeds/export_to_opml_spec.rb' - 'spec/commands/find_new_stories_spec.rb' - - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api/read_favicons_spec.rb' - 'spec/fever_api/read_feeds_groups_spec.rb' diff --git a/spec/controllers/first_run_controller_spec.rb b/spec/controllers/first_run_controller_spec.rb index b449ea83b..7b7a85a59 100644 --- a/spec/controllers/first_run_controller_spec.rb +++ b/spec/controllers/first_run_controller_spec.rb @@ -19,7 +19,6 @@ expect(page).to have_tag("form#password_setup") expect(page).to have_tag("input#password") expect(page).to have_tag("input#password-confirmation") - expect(page).to have_tag("input#submit") end end @@ -66,29 +65,25 @@ page = last_response.body expect(page).to have_tag("#mark-all-instruction") - expect(page).to have_tag("#refresh-instruction") - expect(page).to have_tag("#feeds-instruction") - expect(page).to have_tag("#add-feed-instruction") - expect(page).to have_tag("#story-instruction") - expect(page).to have_tag("#start") end end end context "when a user has been setup" do - it "redirects any requests to first run stuff" do + it "redirects tutorial path to /news" do user = create(:user, :setup_complete) session = { "rack.session" => { user_id: user.id } } - get "/", {}, session + get "/setup/tutorial", {}, session expect(last_response.status).to be(302) expect(URI.parse(last_response.location).path).to eq("/news") + end - get "/setup/password", {}, session - expect(last_response.status).to be(302) - expect(URI.parse(last_response.location).path).to eq("/news") + it "redirects root path to /news" do + user = create(:user, :setup_complete) + session = { "rack.session" => { user_id: user.id } } - get "/setup/tutorial", {}, session + get "/", {}, session expect(last_response.status).to be(302) expect(URI.parse(last_response.location).path).to eq("/news") end From 3a5838f6cf29fe19acbdad0c01baa72fc88c4491 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 1 Jan 2023 21:54:05 -0800 Subject: [PATCH 0463/1107] RuboCop: clean up specs for StoriesController (#770) The latter example wasn't actually testing anything, as the `have_tag` matcher ignores the `class:` option. It's effectively only looking for `li`. There description can't really be tested via controller tests, as the stories are rendered via backbone. --- .rubocop_todo.yml | 1 - spec/controllers/stories_controller_spec.rb | 17 ----------------- 2 files changed, 18 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index bdf9e8f6c..1272e7d1b 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -144,7 +144,6 @@ RSpec/ExampleLength: - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/commands/feeds/export_to_opml_spec.rb' - 'spec/commands/find_new_stories_spec.rb' - - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api/read_favicons_spec.rb' - 'spec/fever_api/read_feeds_groups_spec.rb' - 'spec/fever_api/read_feeds_spec.rb' diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index fb7e7af4b..dad032850 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -46,10 +46,6 @@ page = last_response.body expect(page).to have_tag("a", with: { href: "/feeds/export" }) expect(page).to have_tag("a", with: { href: "/logout" }) - expect(page).to have_tag( - "a", - with: { href: "https://github.com/stringer-rss/stringer" } - ) end it "displays a zen-like message when there are no unread stories" do @@ -166,18 +162,5 @@ expect(last_response.body).to have_tag("#stories") end - - it "differentiates between read and unread" do - allow(FeedRepository).to receive(:fetch).and_return(story_one.feed) - allow(StoryRepository).to receive(:feed).and_return(stories) - - story_one.is_read = false - story_two.is_read = true - - get "/feed/#{story_one.feed.id}" - - expect(last_response.body).to have_tag("li", class: "story") - expect(last_response.body).to have_tag("li", class: "unread") - end end end From 2c77e1cda9141754670d912f71cb004e27c160d9 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Mon, 2 Jan 2023 13:29:22 -0800 Subject: [PATCH 0464/1107] RuboCop: adjust specs for DebugController (#771) --- .rubocop_todo.yml | 1 - spec/controllers/debug_controller_spec.rb | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 1272e7d1b..671f95cec 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -220,7 +220,6 @@ RSpec/MessageExpectation: Exclude: - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/commands/find_new_stories_spec.rb' - - 'spec/controllers/debug_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' diff --git a/spec/controllers/debug_controller_spec.rb b/spec/controllers/debug_controller_spec.rb index f325f0936..3d2652a23 100644 --- a/spec/controllers/debug_controller_spec.rb +++ b/spec/controllers/debug_controller_spec.rb @@ -7,18 +7,20 @@ describe DebugController do describe "GET /debug" do - before do - allow(Delayed::Job).to receive(:count).and_return(42) + def setup + expect(Delayed::Job).to receive(:count).and_return(42) migration_status_instance = instance_double(MigrationStatus) - allow(migration_status_instance) + expect(migration_status_instance) .to receive(:pending_migrations) .and_return(["Migration B - 2", "Migration C - 3"]) - allow(MigrationStatus) + expect(MigrationStatus) .to receive(:new).and_return(migration_status_instance) end it "displays the current Ruby version" do + setup + get "/debug" page = last_response.body @@ -26,6 +28,8 @@ end it "displays the user agent" do + setup + get "/debug", {}, "HTTP_USER_AGENT" => "test" page = last_response.body @@ -33,6 +37,8 @@ end it "displays the delayed job count" do + setup + get "/debug" page = last_response.body @@ -40,6 +46,8 @@ end it "displays pending migrations" do + setup + get "/debug" page = last_response.body From 647a48ae98e334e9d7c7ccace4aacb603ce1839a Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Mon, 2 Jan 2023 14:16:12 -0800 Subject: [PATCH 0465/1107] RuboCop: refactor FirstRunController specs (#773) --- .rubocop_todo.yml | 1 - spec/controllers/first_run_controller_spec.rb | 24 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 671f95cec..3a2059f58 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -220,7 +220,6 @@ RSpec/MessageExpectation: Exclude: - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/commands/find_new_stories_spec.rb' - - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api_spec.rb' diff --git a/spec/controllers/first_run_controller_spec.rb b/spec/controllers/first_run_controller_spec.rb index 7b7a85a59..25b5840e9 100644 --- a/spec/controllers/first_run_controller_spec.rb +++ b/spec/controllers/first_run_controller_spec.rb @@ -7,23 +7,27 @@ describe "FirstRunController" do context "when a user has not been setup" do - before do - allow(UserRepository).to receive(:setup_complete?).and_return(false) + def setup + expect(UserRepository) + .to receive(:setup_complete?).twice.and_return(false) end describe "GET /setup/password" do it "displays a form to enter your password" do + setup + get "/setup/password" page = last_response.body expect(page).to have_tag("form#password_setup") expect(page).to have_tag("input#password") - expect(page).to have_tag("input#password-confirmation") end end describe "POST /setup/password" do it "rejects empty passwords" do + setup + post "/setup/password" page = last_response.body @@ -31,6 +35,8 @@ end it "rejects when password isn't confirmed" do + setup + post "/setup/password", password: "foo", password_confirmation: "bar" page = last_response.body @@ -38,12 +44,12 @@ end it "accepts confirmed passwords and redirects to next step" do + setup user = instance_double(User, id: 1) expect(CreateUser).to receive(:call).with("foo").and_return(user) post "/setup/password", password: "foo", password_confirmation: "foo" - expect(last_response.status).to be(302) expect(URI.parse(last_response.location).path).to eq("/feeds/import") end end @@ -52,16 +58,10 @@ let(:user) { instance_double(User) } let(:feeds) { [instance_double(Feed), instance_double(Feed)] } - before do - allow(UserRepository).to receive(:fetch).and_return(user) - allow(Feed).to receive(:all).and_return(feeds) - end - it "displays the tutorial and completes setup" do - expect(CompleteSetup).to receive(:complete).with(user).once - expect(FetchFeeds).to receive(:enqueue).with(feeds).once + user = create(:user) - get "/setup/tutorial" + get "/setup/tutorial", {}, { "rack.session" => { user_id: user.id } } page = last_response.body expect(page).to have_tag("#mark-all-instruction") From cc83dbe8ce0100f3e5696a0ea51d005ca8123696 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Mon, 2 Jan 2023 15:22:52 -0800 Subject: [PATCH 0466/1107] RuboCop: expect in SessionsController spec (#774) --- .rubocop_todo.yml | 1 - spec/controllers/sessions_controller_spec.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3a2059f58..e9b66019c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -220,7 +220,6 @@ RSpec/MessageExpectation: Exclude: - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/commands/find_new_stories_spec.rb' - - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api_spec.rb' - 'spec/helpers/authentications_helper_spec.rb' diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 589009797..b5e92e5a2 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -17,7 +17,7 @@ describe "POST /login" do it "denies access when password is incorrect" do - allow(SignInUser).to receive(:sign_in).and_return(nil) + expect(SignInUser).to receive(:sign_in).and_return(nil) post "/login", password: "not-the-password" From 5aa103b90f0deb7d3038075a3647071a5dcc4722 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Mon, 2 Jan 2023 15:35:58 -0800 Subject: [PATCH 0467/1107] RuboCop: clean up StoriesController spec (#775) --- .rubocop_todo.yml | 1 - spec/controllers/stories_controller_spec.rb | 41 ++++++++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e9b66019c..bbc72ca15 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -220,7 +220,6 @@ RSpec/MessageExpectation: Exclude: - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/commands/find_new_stories_spec.rb' - - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api_spec.rb' - 'spec/helpers/authentications_helper_spec.rb' - 'spec/models/migration_status_spec.rb' diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index dad032850..595806394 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -11,19 +11,21 @@ let(:stories) { [story_one, story_two] } describe "GET /news" do - before do - allow(StoryRepository).to receive(:unread).and_return(stories) - allow(UserRepository).to receive(:fetch).and_return(double) + def setup + expect(StoryRepository).to receive(:unread).and_return(stories) + expect(UserRepository).to receive(:fetch).twice.and_return(double) end it "display list of unread stories" do + setup + get "/news" expect(last_response.body).to have_tag("#stories") end it "displays the blog title and article title" do - expect(StoryRepository).to receive(:unread).and_return([story_one]) + setup get "/news" @@ -32,15 +34,18 @@ end it "displays all user actions" do + setup + get "/news" expect(last_response.body).to have_tag("#mark-all") expect(last_response.body).to have_tag("#refresh") expect(last_response.body).to have_tag("#feeds") - expect(last_response.body).to have_tag("#add-feed") end it "has correct footer links" do + setup + get "/news" page = last_response.body @@ -49,7 +54,7 @@ end it "displays a zen-like message when there are no unread stories" do - allow(StoryRepository).to receive(:unread).and_return([]) + expect(StoryRepository).to receive(:unread).and_return([]) get "/news" @@ -62,9 +67,9 @@ let(:read_two) { build(:story, :read) } let(:stories) { [read_one, read_two].paginate } - before { allow(StoryRepository).to receive(:read).and_return(stories) } - it "displays the list of read stories with pagination" do + expect(StoryRepository).to receive(:read).and_return(stories) + get "/archive" page = last_response.body @@ -78,9 +83,9 @@ let(:starred_two) { build(:story, :starred) } let(:stories) { [starred_one, starred_two].paginate } - before { allow(StoryRepository).to receive(:starred).and_return(stories) } - it "displays the list of starred stories with pagination" do + expect(StoryRepository).to receive(:starred).and_return(stories) + get "/starred" page = last_response.body @@ -90,9 +95,8 @@ end describe "PUT /stories/:id" do - before { allow(StoryRepository).to receive(:fetch).and_return(story_one) } - it "marks a story as read when it is_read not malformed" do + expect(StoryRepository).to receive(:fetch).and_return(story_one) expect(story_one).to receive(:save!).once put "/stories/#{story_one.id}", { is_read: true }.to_json @@ -101,6 +105,7 @@ end it "marks a story as read when is_read is malformed" do + expect(StoryRepository).to receive(:fetch).and_return(story_one) expect(story_one).to receive(:save!).once put "/stories/#{story_one.id}", { is_read: "malformed" }.to_json @@ -109,24 +114,32 @@ end it "marks a story as keep unread when it keep_unread not malformed" do + expect(StoryRepository).to receive(:fetch).and_return(story_one) + put "/stories/#{story_one.id}", { keep_unread: false }.to_json expect(story_one.keep_unread).to be(false) end it "marks a story as keep unread when keep_unread is malformed" do + expect(StoryRepository).to receive(:fetch).and_return(story_one) + put "/stories/#{story_one.id}", { keep_unread: "malformed" }.to_json expect(story_one.keep_unread).to be(true) end it "marks a story as starred when is_starred is not malformed" do + expect(StoryRepository).to receive(:fetch).and_return(story_one) + put "/stories/#{story_one.id}", { is_starred: true }.to_json expect(story_one.is_starred).to be(true) end it "marks a story as starred when is_starred is malformed" do + expect(StoryRepository).to receive(:fetch).and_return(story_one) + put "/stories/#{story_one.id}", { is_starred: "malformed" }.to_json expect(story_one.is_starred).to be(true) @@ -155,8 +168,8 @@ end it "displays a list of stories" do - allow(FeedRepository).to receive(:fetch).and_return(story_one.feed) - allow(StoryRepository).to receive(:feed).and_return(stories) + expect(FeedRepository).to receive(:fetch).and_return(story_one.feed) + expect(StoryRepository).to receive(:feed).and_return(stories) get "/feed/#{story_one.feed.id}" From 0892124fce23ee9f4e4f7ba294b92a7310b9ac06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Jan 2023 08:04:09 -0800 Subject: [PATCH 0468/1107] Bump httparty from 0.20.0 to 0.21.0 (#776) Bumps [httparty](https://github.com/jnunemaker/httparty) from 0.20.0 to 0.21.0. - [Release notes](https://github.com/jnunemaker/httparty/releases) - [Changelog](https://github.com/jnunemaker/httparty/blob/master/Changelog.md) - [Commits](https://github.com/jnunemaker/httparty/compare/v0.20.0...v0.21.0) --- updated-dependencies: - dependency-name: httparty dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5b64567b3..ab25f3644 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -114,8 +114,8 @@ GEM ffi (1.15.5) globalid (1.0.0) activesupport (>= 5.0) - httparty (0.20.0) - mime-types (~> 3.0) + httparty (0.21.0) + mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) i18n (1.12.0) concurrent-ruby (~> 1.0) @@ -131,9 +131,6 @@ GEM marcel (1.0.2) matrix (0.4.2) method_source (1.0.0) - mime-types (3.4.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) mini_mime (1.1.2) mini_portile2 (2.8.1) minitest (5.16.3) From 209fb677805a04231152fec274839944c1bf2b3f Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 15:08:43 -0800 Subject: [PATCH 0469/1107] RuboCop: refactor in DebugController spec (#777) --- .rubocop.yml | 1 + .rubocop_todo.yml | 1 - spec/controllers/debug_controller_spec.rb | 6 +++--- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index fe813b383..de898a51e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -18,6 +18,7 @@ RSpec/MessageExpectation: { EnforcedStyle: expect } RSpec/MessageSpies: { EnforcedStyle: receive } Style/MethodCallWithArgsParentheses: AllowedMethods: + - and - to - not_to - describe diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index bbc72ca15..4674c8902 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -249,7 +249,6 @@ RSpec/MultipleExpectations: - 'spec/commands/users/change_user_password_spec.rb' - 'spec/commands/users/complete_setup_spec.rb' - 'spec/commands/users/create_user_spec.rb' - - 'spec/controllers/debug_controller_spec.rb' - 'spec/controllers/feeds_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/imports_controller_spec.rb' diff --git a/spec/controllers/debug_controller_spec.rb b/spec/controllers/debug_controller_spec.rb index 3d2652a23..f2fafc359 100644 --- a/spec/controllers/debug_controller_spec.rb +++ b/spec/controllers/debug_controller_spec.rb @@ -50,9 +50,9 @@ def setup get "/debug" - page = last_response.body - expect(page).to have_tag("li", text: /Migration B - 2/) - expect(page).to have_tag("li", text: /Migration C - 3/) + rendered = Capybara.string(last_response.body) + expect(rendered).to have_selector("li", text: /Migration B - 2/) + .and have_selector("li", text: /Migration C - 3/) end end From 7515fb2eeea1fa8cbeb567fca68b3fa362a2bda9 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 15:12:59 -0800 Subject: [PATCH 0470/1107] Specs: set up Webmock (#778) Prevent network connections. It's really easily to accidentally invoke the `FeedDiscovery` class, which hits the network, so this will make sure we don't forget. --- Gemfile | 1 + Gemfile.lock | 8 ++++++++ spec/spec_helper.rb | 1 + spec/support/webmock.rb | 8 ++++++++ 4 files changed, 18 insertions(+) create mode 100644 spec/support/webmock.rb diff --git a/Gemfile b/Gemfile index 2f91f7809..246d875eb 100644 --- a/Gemfile +++ b/Gemfile @@ -46,4 +46,5 @@ group :development, :test do gem "shotgun" gem "simplecov" gem "timecop" + gem "webmock", require: false end diff --git a/Gemfile.lock b/Gemfile.lock index ab25f3644..4f0243833 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -88,6 +88,8 @@ GEM term-ansicolor (~> 1.7) thor (~> 1.2) tins (~> 1.32) + crack (0.4.5) + rexml crass (1.0.6) date (3.3.3) delayed_job (4.1.11) @@ -114,6 +116,7 @@ GEM ffi (1.15.5) globalid (1.0.0) activesupport (>= 5.0) + hashdiff (1.0.1) httparty (0.21.0) mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) @@ -301,6 +304,10 @@ GEM uglifier (4.2.0) execjs (>= 0.3.0, < 3) unicode-display_width (2.3.0) + webmock (3.18.1) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -348,6 +355,7 @@ DEPENDENCIES thread timecop uglifier + webmock will_paginate RUBY VERSION diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9688013bb..66d5bbe7f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,6 +15,7 @@ require_relative "support/coverage" require_relative "support/factory_bot" +require_relative "support/webmock" require_relative "factories" require "./app" diff --git a/spec/support/webmock.rb b/spec/support/webmock.rb new file mode 100644 index 000000000..4e0581096 --- /dev/null +++ b/spec/support/webmock.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "webmock/rspec" + +WebMock.disable_net_connect!( + allow_localhost: true, + allow: [/geckodriver/, /chromedriver/] +) From 5ff04603a4df81c0dd78e8ae770a9b552edf35f6 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 15:19:43 -0800 Subject: [PATCH 0471/1107] RuboCop: use shorter hash syntax (#779) --- .rubocop_todo.yml | 38 --------------------- app.rb | 4 +-- app/commands/users/create_user.rb | 2 +- app/fever_api/read_favicons.rb | 2 +- app/fever_api/read_feeds.rb | 2 +- app/fever_api/read_feeds_groups.rb | 4 +-- app/fever_api/read_groups.rb | 2 +- app/fever_api/read_links.rb | 2 +- app/fever_api/sync_saved_item_ids.rb | 2 +- app/fever_api/sync_unread_item_ids.rb | 2 +- app/models/feed.rb | 4 +-- app/models/group.rb | 2 +- app/models/story.rb | 6 ++-- app/repositories/story_repository.rb | 10 +++--- app/utils/sample_story.rb | 20 +++++------ spec/fever_api/read_feeds_groups_spec.rb | 4 +-- spec/fever_api/read_feeds_spec.rb | 4 +-- spec/fever_api/read_groups_spec.rb | 2 +- spec/fever_api/read_items_spec.rb | 2 +- spec/fever_api/sync_saved_item_ids_spec.rb | 6 ++-- spec/fever_api/sync_unread_item_ids_spec.rb | 6 ++-- spec/fever_api/write_mark_feed_spec.rb | 2 +- spec/fever_api/write_mark_group_spec.rb | 2 +- spec/fever_api_spec.rb | 6 ++-- spec/integration/feed_importing_spec.rb | 2 +- spec/javascript/test_controller.rb | 4 +-- spec/models/feed_spec.rb | 2 +- spec/models/story_spec.rb | 8 ++--- spec/repositories/story_repository_spec.rb | 26 +++++++------- spec/tasks/change_password_spec.rb | 4 +-- spec/tasks/fetch_feed_spec.rb | 32 ++++------------- 31 files changed, 76 insertions(+), 138 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 4674c8902..ab63311fc 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -511,44 +511,6 @@ Style/FetchEnvVar: Exclude: - 'Rakefile' -# Offense count: 86 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. -# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys -# SupportedShorthandSyntax: always, never, either, consistent -Style/HashSyntax: - Exclude: - - 'app.rb' - - 'app/commands/users/create_user.rb' - - 'app/fever_api/read_favicons.rb' - - 'app/fever_api/read_feeds.rb' - - 'app/fever_api/read_feeds_groups.rb' - - 'app/fever_api/read_groups.rb' - - 'app/fever_api/read_links.rb' - - 'app/fever_api/sync_saved_item_ids.rb' - - 'app/fever_api/sync_unread_item_ids.rb' - - 'app/models/feed.rb' - - 'app/models/group.rb' - - 'app/models/story.rb' - - 'app/repositories/story_repository.rb' - - 'app/utils/sample_story.rb' - - 'spec/fever_api/read_feeds_groups_spec.rb' - - 'spec/fever_api/read_feeds_spec.rb' - - 'spec/fever_api/read_groups_spec.rb' - - 'spec/fever_api/read_items_spec.rb' - - 'spec/fever_api/sync_saved_item_ids_spec.rb' - - 'spec/fever_api/sync_unread_item_ids_spec.rb' - - 'spec/fever_api/write_mark_feed_spec.rb' - - 'spec/fever_api/write_mark_group_spec.rb' - - 'spec/fever_api_spec.rb' - - 'spec/integration/feed_importing_spec.rb' - - 'spec/javascript/test_controller.rb' - - 'spec/models/feed_spec.rb' - - 'spec/models/story_spec.rb' - - 'spec/repositories/story_repository_spec.rb' - - 'spec/tasks/change_password_spec.rb' - - 'spec/tasks/fetch_feed_spec.rb' - # Offense count: 184 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: IgnoreMacros, AllowedMethods, IgnoredMethods, AllowedPatterns, IgnoredPatterns, IncludedMacros, AllowParenthesesInMultilineCall, AllowParenthesesInChaining, AllowParenthesesInCamelCaseMethod, AllowParenthesesInStringInterpolation, EnforcedStyle. diff --git a/app.rb b/app.rb index 87db2d0c4..24225afb0 100644 --- a/app.rb +++ b/app.rb @@ -69,7 +69,7 @@ class Stringer < Sinatra::Base include Sinatra::AuthenticationHelpers def render_partial(name, locals = {}) - erb "partials/_#{name}".to_sym, layout: false, locals: locals + erb "partials/_#{name}".to_sym, layout: false, locals: end def render_js_template(name) @@ -77,7 +77,7 @@ def render_js_template(name) end def render_js(name, locals = {}) - erb "js/#{name}.js".to_sym, layout: false, locals: locals + erb "js/#{name}.js".to_sym, layout: false, locals: end def t(*args, **kwargs) diff --git a/app/commands/users/create_user.rb b/app/commands/users/create_user.rb index 955d615de..cb1765ff7 100644 --- a/app/commands/users/create_user.rb +++ b/app/commands/users/create_user.rb @@ -14,7 +14,7 @@ def self.call(password) def call(password) @repo.delete_all @repo.create( - password: password, + password:, password_confirmation: password, setup_complete: false, api_key: ApiKey.compute(password) diff --git a/app/fever_api/read_favicons.rb b/app/fever_api/read_favicons.rb index 6b5a8dc58..1f5de9166 100644 --- a/app/fever_api/read_favicons.rb +++ b/app/fever_api/read_favicons.rb @@ -6,7 +6,7 @@ class ReadFavicons def call(params = {}) if params.keys.include?("favicons") - { favicons: favicons } + { favicons: } else {} end diff --git a/app/fever_api/read_feeds.rb b/app/fever_api/read_feeds.rb index cabba321f..aab68c452 100644 --- a/app/fever_api/read_feeds.rb +++ b/app/fever_api/read_feeds.rb @@ -10,7 +10,7 @@ def initialize(options = {}) def call(params = {}) if params.keys.include?("feeds") - { feeds: feeds } + { feeds: } else {} end diff --git a/app/fever_api/read_feeds_groups.rb b/app/fever_api/read_feeds_groups.rb index d6597a293..d13a8fbd8 100644 --- a/app/fever_api/read_feeds_groups.rb +++ b/app/fever_api/read_feeds_groups.rb @@ -10,7 +10,7 @@ def initialize(options = {}) def call(params = {}) if params.keys.include?("feeds") || params.keys.include?("groups") - { feeds_groups: feeds_groups } + { feeds_groups: } else {} end @@ -23,7 +23,7 @@ def feeds_groups @feed_repository.in_group.order("LOWER(name)").group_by(&:group_id) grouped_feeds.map do |group_id, feeds| { - group_id: group_id, + group_id:, feed_ids: feeds.map(&:id).join(",") } end diff --git a/app/fever_api/read_groups.rb b/app/fever_api/read_groups.rb index 2540195f1..b5d7d343e 100644 --- a/app/fever_api/read_groups.rb +++ b/app/fever_api/read_groups.rb @@ -10,7 +10,7 @@ def initialize(options = {}) def call(params = {}) if params.keys.include?("groups") - { groups: groups } + { groups: } else {} end diff --git a/app/fever_api/read_links.rb b/app/fever_api/read_links.rb index 1377b5b15..8ba078604 100644 --- a/app/fever_api/read_links.rb +++ b/app/fever_api/read_links.rb @@ -4,7 +4,7 @@ module FeverAPI class ReadLinks def call(params = {}) if params.keys.include?("links") - { links: links } + { links: } else {} end diff --git a/app/fever_api/sync_saved_item_ids.rb b/app/fever_api/sync_saved_item_ids.rb index e169a1c5e..610f80ce0 100644 --- a/app/fever_api/sync_saved_item_ids.rb +++ b/app/fever_api/sync_saved_item_ids.rb @@ -10,7 +10,7 @@ def initialize(options = {}) def call(params = {}) if params.keys.include?("saved_item_ids") - { saved_item_ids: saved_item_ids } + { saved_item_ids: } else {} end diff --git a/app/fever_api/sync_unread_item_ids.rb b/app/fever_api/sync_unread_item_ids.rb index 84a043a83..1e06f10cb 100644 --- a/app/fever_api/sync_unread_item_ids.rb +++ b/app/fever_api/sync_unread_item_ids.rb @@ -10,7 +10,7 @@ def initialize(options = {}) def call(params = {}) if params.keys.include?("unread_item_ids") - { unread_item_ids: unread_item_ids } + { unread_item_ids: } else {} end diff --git a/app/models/feed.rb b/app/models/feed.rb index 3e4716405..266bd32e2 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -22,10 +22,10 @@ def unread_stories def as_fever_json { - id: id, + id:, favicon_id: 0, title: name, - url: url, + url:, site_url: url, is_spark: 0, last_updated_on_time: last_fetched.to_i diff --git a/app/models/group.rb b/app/models/group.rb index 3dbcf1b96..0976ce6b6 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -6,6 +6,6 @@ class Group < ApplicationRecord has_many :feeds def as_fever_json - { id: id, title: name } + { id:, title: name } end end diff --git a/app/models/story.rb b/app/models/story.rb index ea3055e79..18784be7f 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -32,9 +32,9 @@ def as_json(_options = {}) def as_fever_json { - id: id, - feed_id: feed_id, - title: title, + id:, + feed_id:, + title:, author: source, html: body, url: permalink, diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index a2c1032c0..1380b7045 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -11,10 +11,10 @@ class StoryRepository def self.add(entry, feed) enclosure_url = entry.enclosure_url if entry.respond_to?(:enclosure_url) Story.create( - feed: feed, + feed:, title: extract_title(entry), permalink: extract_url(entry, feed), - enclosure_url: enclosure_url, + enclosure_url:, body: extract_content(entry), is_read: false, is_starred: false, @@ -38,18 +38,18 @@ def self.fetch_unread_by_timestamp(timestamp) def self.fetch_unread_by_timestamp_and_group(timestamp, group_id) fetch_unread_by_timestamp(timestamp) - .joins(:feed).where(feeds: { group_id: group_id }) + .joins(:feed).where(feeds: { group_id: }) end def self.fetch_unread_for_feed_by_timestamp(feed_id, timestamp) timestamp = Time.at(timestamp.to_i) Story - .where(feed_id: feed_id) + .where(feed_id:) .where("created_at < ? AND is_read = ?", timestamp, false) end def self.exists?(id, feed_id) - Story.exists?(entry_id: id, feed_id: feed_id) + Story.exists?(entry_id: id, feed_id:) end def self.unread diff --git a/app/utils/sample_story.rb b/app/utils/sample_story.rb index 6b67f6fbe..1bde2143d 100644 --- a/app/utils/sample_story.rb +++ b/app/utils/sample_story.rb @@ -61,17 +61,17 @@ def published def as_json(_options = {}) { - id: id, - headline: headline, - lead: lead, - source: source, - title: title, + id:, + headline:, + lead:, + source:, + title:, pretty_date: published.strftime("%A, %B %d"), - body: body, - permalink: permalink, - is_read: is_read, - is_starred: is_starred, - keep_unread: keep_unread + body:, + permalink:, + is_read:, + is_starred:, + keep_unread: } end end diff --git a/spec/fever_api/read_feeds_groups_spec.rb b/spec/fever_api/read_feeds_groups_spec.rb index 4df971b5f..e4004fd20 100644 --- a/spec/fever_api/read_feeds_groups_spec.rb +++ b/spec/fever_api/read_feeds_groups_spec.rb @@ -6,10 +6,10 @@ describe FeverAPI::ReadFeedsGroups do let(:feed_ids) { [5, 7, 11] } - let(:feeds) { feed_ids.map { |id| double("feed", id: id, group_id: 1) } } + let(:feeds) { feed_ids.map { |id| double("feed", id:, group_id: 1) } } let(:feed_repository) { double("repo") } - subject { FeverAPI::ReadFeedsGroups.new(feed_repository: feed_repository) } + subject { FeverAPI::ReadFeedsGroups.new(feed_repository:) } it "returns a list of groups requested through feeds" do allow(feed_repository) diff --git a/spec/fever_api/read_feeds_spec.rb b/spec/fever_api/read_feeds_spec.rb index 40732e031..f429153d4 100644 --- a/spec/fever_api/read_feeds_spec.rb +++ b/spec/fever_api/read_feeds_spec.rb @@ -7,11 +7,11 @@ describe FeverAPI::ReadFeeds do let(:feed_ids) { [5, 7, 11] } let(:feeds) do - feed_ids.map { |id| double("feed", id: id, as_fever_json: { id: id }) } + feed_ids.map { |id| double("feed", id:, as_fever_json: { id: }) } end let(:feed_repository) { double("repo") } - subject { FeverAPI::ReadFeeds.new(feed_repository: feed_repository) } + subject { FeverAPI::ReadFeeds.new(feed_repository:) } it "returns a list of feeds" do expect(feed_repository).to receive(:list).and_return(feeds) diff --git a/spec/fever_api/read_groups_spec.rb b/spec/fever_api/read_groups_spec.rb index 2b7b5951d..d36002995 100644 --- a/spec/fever_api/read_groups_spec.rb +++ b/spec/fever_api/read_groups_spec.rb @@ -11,7 +11,7 @@ end let(:group_repository) { double("repo") } - subject { FeverAPI::ReadGroups.new(group_repository: group_repository) } + subject { FeverAPI::ReadGroups.new(group_repository:) } it "returns a group list if requested" do expect(group_repository).to receive(:list).and_return([group1, group2]) diff --git a/spec/fever_api/read_items_spec.rb b/spec/fever_api/read_items_spec.rb index a44a316f0..caa375f44 100644 --- a/spec/fever_api/read_items_spec.rb +++ b/spec/fever_api/read_items_spec.rb @@ -7,7 +7,7 @@ describe FeverAPI::ReadItems do let(:story_repository) { double("repo") } - subject { FeverAPI::ReadItems.new(story_repository: story_repository) } + subject { FeverAPI::ReadItems.new(story_repository:) } it "returns a list of unread items including total count" do stories = [ diff --git a/spec/fever_api/sync_saved_item_ids_spec.rb b/spec/fever_api/sync_saved_item_ids_spec.rb index 6b8afc0f9..6acfe5e0a 100644 --- a/spec/fever_api/sync_saved_item_ids_spec.rb +++ b/spec/fever_api/sync_saved_item_ids_spec.rb @@ -6,12 +6,10 @@ describe FeverAPI::SyncSavedItemIds do let(:story_ids) { [5, 7, 11] } - let(:stories) { story_ids.map { |id| double("story", id: id) } } + let(:stories) { story_ids.map { |id| double("story", id:) } } let(:story_repository) { double("repo") } - subject do - FeverAPI::SyncSavedItemIds.new(story_repository: story_repository) - end + subject { FeverAPI::SyncSavedItemIds.new(story_repository:) } it "returns a list of starred items if requested" do expect(story_repository).to receive(:all_starred).and_return(stories) diff --git a/spec/fever_api/sync_unread_item_ids_spec.rb b/spec/fever_api/sync_unread_item_ids_spec.rb index 8bb197dbc..f84c22ac4 100644 --- a/spec/fever_api/sync_unread_item_ids_spec.rb +++ b/spec/fever_api/sync_unread_item_ids_spec.rb @@ -6,12 +6,10 @@ describe FeverAPI::SyncUnreadItemIds do let(:story_ids) { [5, 7, 11] } - let(:stories) { story_ids.map { |id| double("story", id: id) } } + let(:stories) { story_ids.map { |id| double("story", id:) } } let(:story_repository) { double("repo") } - subject do - FeverAPI::SyncUnreadItemIds.new(story_repository: story_repository) - end + subject { FeverAPI::SyncUnreadItemIds.new(story_repository:) } it "returns a list of unread items if requested" do expect(story_repository).to receive(:unread).and_return(stories) diff --git a/spec/fever_api/write_mark_feed_spec.rb b/spec/fever_api/write_mark_feed_spec.rb index 2a6c252e4..2ff1e4546 100644 --- a/spec/fever_api/write_mark_feed_spec.rb +++ b/spec/fever_api/write_mark_feed_spec.rb @@ -8,7 +8,7 @@ let(:feed_marker) { double("feed marker") } let(:marker_class) { double("marker class") } - subject { FeverAPI::WriteMarkFeed.new(marker_class: marker_class) } + subject { FeverAPI::WriteMarkFeed.new(marker_class:) } it "instantiates a feed marker and calls mark_feed_as_read if requested" do expect(marker_class) diff --git a/spec/fever_api/write_mark_group_spec.rb b/spec/fever_api/write_mark_group_spec.rb index b86b42f98..0713f19c4 100644 --- a/spec/fever_api/write_mark_group_spec.rb +++ b/spec/fever_api/write_mark_group_spec.rb @@ -8,7 +8,7 @@ let(:group_marker) { double("group marker") } let(:marker_class) { double("marker class") } - subject { FeverAPI::WriteMarkGroup.new(marker_class: marker_class) } + subject { FeverAPI::WriteMarkGroup.new(marker_class:) } it "instantiates a group marker and calls mark_group_as_read if requested" do expect(marker_class) diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index aa99b0df5..88df51341 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -14,16 +14,16 @@ def app let(:story_one) { build(:story) } let(:story_two) { build(:story) } let(:group) { build(:group) } - let(:feed) { build(:feed, group: group) } + let(:feed) { build(:feed, group:) } let(:stories) { [story_one, story_two] } let(:standard_answer) do { api_version: 3, auth: 1, last_refreshed_on_time: 123456789 } end let(:cannot_auth) { { api_version: 3, auth: 0 } } - let(:headers) { { api_key: api_key } } + let(:headers) { { api_key: } } before do - user = double(api_key: api_key) + user = double(api_key:) allow(User).to receive(:first) { user } allow(Time).to receive(:now) { Time.at(123456789) } diff --git a/spec/integration/feed_importing_spec.rb b/spec/integration/feed_importing_spec.rb index 1831528db..d3b09acea 100644 --- a/spec/integration/feed_importing_spec.rb +++ b/spec/integration/feed_importing_spec.rb @@ -88,5 +88,5 @@ def fetch_feed(feed) logger = Logger.new($stdout) logger.level = Logger::DEBUG - FetchFeed.new(feed, logger: logger).fetch + FetchFeed.new(feed, logger:).fetch end diff --git a/spec/javascript/test_controller.rb b/spec/javascript/test_controller.rb index 3b1a7358d..014976308 100644 --- a/spec/javascript/test_controller.rb +++ b/spec/javascript/test_controller.rb @@ -9,8 +9,8 @@ def self.test_path(*chunks) erb File.read(self.class.test_path("support", "views", "index.erb")), layout: false, locals: { - js_files: js_files, - js_templates: js_templates + js_files:, + js_templates: } end diff --git a/spec/models/feed_spec.rb b/spec/models/feed_spec.rb index 7acdc0501..e116c6e38 100644 --- a/spec/models/feed_spec.rb +++ b/spec/models/feed_spec.rb @@ -62,7 +62,7 @@ id: 52, name: "chicken feed", url: "wat url", - last_fetched: last_fetched + last_fetched: ) expect(feed.as_fever_json).to eq( diff --git a/spec/models/story_spec.rb b/spec/models/story_spec.rb index e015054c9..262f0ba34 100644 --- a/spec/models/story_spec.rb +++ b/spec/models/story_spec.rb @@ -74,16 +74,16 @@ story = create( :story, body: "story body", - created_at: created_at, + created_at:, entry_id: 5, - feed: feed, + feed:, is_read: true, is_starred: false, keep_unread: true, permalink: "www.exampoo.com/perma", published: published_at, title: "the story title", - updated_at: updated_at + updated_at: ) expect(story.as_json).to eq( @@ -116,7 +116,7 @@ published_at = 1.day.ago story = create( :story, - feed: feed, + feed:, title: "the story title", body: "story body", permalink: "www.exampoo.com/perma", diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index 6b4abc5ad..b3c806901 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -121,7 +121,7 @@ describe ".fetch_unread_by_timestamp_and_group" do it "returns unread stories before timestamp for group_id" do feed = create(:feed, group_id: 52) - story = create(:story, :unread, feed: feed, created_at: 5.minutes.ago) + story = create(:story, :unread, feed:, created_at: 5.minutes.ago) time = Time.now stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, 52) @@ -131,7 +131,7 @@ it "does not return read stories before timestamp for group_id" do feed = create(:feed, group_id: 52) - create(:story, feed: feed, created_at: 5.minutes.ago) + create(:story, feed:, created_at: 5.minutes.ago) time = Time.now stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, 52) @@ -141,7 +141,7 @@ it "does not return unread stories after timestamp for group_id" do feed = create(:feed, group_id: 52) - create(:story, :unread, feed: feed, created_at: 5.minutes.ago) + create(:story, :unread, feed:, created_at: 5.minutes.ago) time = 6.minutes.ago stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, 52) @@ -151,7 +151,7 @@ it "does not return stories before timestamp for other group_id" do feed = create(:feed, group_id: 52) - create(:story, :unread, feed: feed, created_at: 5.minutes.ago) + create(:story, :unread, feed:, created_at: 5.minutes.ago) time = Time.now stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, 55) @@ -161,7 +161,7 @@ it "does not return stories with no group_id before timestamp" do feed = create(:feed) - create(:story, :unread, feed: feed, created_at: 5.minutes.ago) + create(:story, :unread, feed:, created_at: 5.minutes.ago) time = Time.now stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, 52) @@ -171,7 +171,7 @@ it "returns unread stories before timestamp for nil group_id" do feed = create(:feed) - story = create(:story, :unread, feed: feed, created_at: 5.minutes.ago) + story = create(:story, :unread, feed:, created_at: 5.minutes.ago) time = Time.now stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, nil) @@ -183,7 +183,7 @@ describe ".fetch_unread_for_feed_by_timestamp" do it "returns unread stories for the feed before timestamp" do feed = create(:feed) - story = create(:story, :unread, feed: feed, created_at: 5.minutes.ago) + story = create(:story, :unread, feed:, created_at: 5.minutes.ago) time = 4.minutes.ago stories = @@ -194,7 +194,7 @@ it "returns unread stories for the feed before string timestamp" do feed = create(:feed) - story = create(:story, :unread, feed: feed, created_at: 5.minutes.ago) + story = create(:story, :unread, feed:, created_at: 5.minutes.ago) timestamp = Integer(4.minutes.ago).to_s stories = @@ -205,7 +205,7 @@ it "does not return read stories for the feed before timestamp" do feed = create(:feed) - create(:story, feed: feed, created_at: 5.minutes.ago) + create(:story, feed:, created_at: 5.minutes.ago) time = 4.minutes.ago stories = @@ -216,7 +216,7 @@ it "does not return unread stories for the feed after timestamp" do feed = create(:feed) - create(:story, :unread, feed: feed, created_at: 5.minutes.ago) + create(:story, :unread, feed:, created_at: 5.minutes.ago) time = 6.minutes.ago stories = @@ -279,15 +279,15 @@ describe ".feed" do it "returns stories for the given feed id" do feed = create(:feed) - story = create(:story, feed: feed) + story = create(:story, feed:) expect(StoryRepository.feed(feed.id)).to eq([story]) end it "sorts stories by published" do feed = create(:feed) - story1 = create(:story, feed: feed, published: 1.day.ago) - story2 = create(:story, feed: feed, published: 1.hour.ago) + story1 = create(:story, feed:, published: 1.day.ago) + story2 = create(:story, feed:, published: 1.hour.ago) expect(StoryRepository.feed(feed.id)).to eq([story2, story1]) end diff --git a/spec/tasks/change_password_spec.rb b/spec/tasks/change_password_spec.rb index 648a64858..ef6a5e08b 100644 --- a/spec/tasks/change_password_spec.rb +++ b/spec/tasks/change_password_spec.rb @@ -11,7 +11,7 @@ it "invokes command with confirmed password" do output = StringIO.new input = StringIO.new("new-pw\nnew-pw\n") - task = ChangePassword.new(command, output: output, input: input) + task = ChangePassword.new(command, output:, input:) expect(command).to receive(:change_user_password).with("new-pw") @@ -21,7 +21,7 @@ it "repeats until a matching confirmation" do output = StringIO.new input = StringIO.new("woops\nnope\nnew-pw\nnew-pw\n") - task = ChangePassword.new(command, output: output, input: input) + task = ChangePassword.new(command, output:, input:) expect(command).to receive(:change_user_password).with("new-pw") diff --git a/spec/tasks/fetch_feed_spec.rb b/spec/tasks/fetch_feed_spec.rb index 457d80e0b..532a69166 100644 --- a/spec/tasks/fetch_feed_spec.rb +++ b/spec/tasks/fetch_feed_spec.rb @@ -27,12 +27,7 @@ expect(StoryRepository).not_to receive(:add) - FetchFeed.new( - daring_fireball, - parser: parser, - client: client, - logger: nil - ).fetch + FetchFeed.new(daring_fireball, parser:, client:, logger: nil).fetch end it "logs a message" do @@ -41,12 +36,7 @@ output = StringIO.new logger = Logger.new(output) - FetchFeed.new( - daring_fireball, - parser: parser, - client: client, - logger: - ).fetch + FetchFeed.new(daring_fireball, parser:, client:, logger:).fetch expect(output.string).to include("has not been modified") end @@ -63,7 +53,7 @@ expect(StoryRepository).not_to receive(:add) - FetchFeed.new(daring_fireball, parser: parser, client: client).fetch + FetchFeed.new(daring_fireball, parser:, client:).fetch end end @@ -119,7 +109,7 @@ expect(FeedRepository).to receive(:set_status) .with(:green, daring_fireball) - FetchFeed.new(daring_fireball, parser: parser, client: client).fetch + FetchFeed.new(daring_fireball, parser:, client:).fetch end it "sets the status to red if things go wrong" do @@ -129,12 +119,7 @@ expect(FeedRepository).to receive(:set_status) .with(:red, daring_fireball) - FetchFeed.new( - daring_fireball, - parser: parser, - client: client, - logger: nil - ).fetch + FetchFeed.new(daring_fireball, parser:, client:, logger: nil).fetch end it "outputs a message when things go wrong" do @@ -143,12 +128,7 @@ output = StringIO.new logger = Logger.new(output) - FetchFeed.new( - daring_fireball, - parser: parser, - client: client, - logger: logger - ).fetch + FetchFeed.new(daring_fireball, parser:, client:, logger:).fetch expect(output.string).to include("Something went wrong") end From 145f37aa0110920c0e8d6fff12e4daced0ed201e Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 15:28:00 -0800 Subject: [PATCH 0472/1107] RuboCop: reduce expectations in feeds controller spec (#780) --- .rubocop_todo.yml | 1 - spec/controllers/feeds_controller_spec.rb | 51 ++++++++++------------- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index ab63311fc..b1530a98c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -249,7 +249,6 @@ RSpec/MultipleExpectations: - 'spec/commands/users/change_user_password_spec.rb' - 'spec/commands/users/complete_setup_spec.rb' - 'spec/commands/users/create_user_spec.rb' - - 'spec/controllers/feeds_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/imports_controller_spec.rb' - 'spec/controllers/sessions_controller_spec.rb' diff --git a/spec/controllers/feeds_controller_spec.rb b/spec/controllers/feeds_controller_spec.rb index 6ac77e2ae..b4e7ac780 100644 --- a/spec/controllers/feeds_controller_spec.rb +++ b/spec/controllers/feeds_controller_spec.rb @@ -5,22 +5,17 @@ app_require "controllers/feeds_controller" describe "FeedsController" do - let(:feeds) { build_pair(:feed) } - describe "GET /feeds" do it "renders a list of feeds" do - expect(FeedRepository).to receive(:list).and_return(feeds) + create_pair(:feed) get "/feeds" - page = last_response.body - expect(page).to have_tag("ul#feed-list") - expect(page).to have_tag("li.feed", count: 2) + rendered = Capybara.string(last_response.body) + expect(rendered).to have_selector("li.feed", count: 2) end it "displays message to add feeds if there are none" do - expect(FeedRepository).to receive(:list).and_return([]) - get "/feeds" page = last_response.body @@ -29,14 +24,13 @@ end describe "GET /feeds/:feed_id/edit" do - it "fetches a feed given the id" do - feed = Feed.new(name: "Rainbows and unicorns", url: "example.com/feed") - expect(FeedRepository).to receive(:fetch).with("123").and_return(feed) + it "displays the feed edit form" do + feed = create(:feed, name: "Rainbows/unicorns", url: "example.com/feed") - get "/feeds/123/edit" + get "/feeds/#{feed.id}/edit" - expect(last_response.body).to include("Rainbows and unicorns") - expect(last_response.body).to include("example.com/feed") + rendered = Capybara.string(last_response.body) + expect(rendered).to have_field("feed_name", with: "Rainbows/unicorns") end end @@ -90,37 +84,37 @@ def params(feed, **overrides) end describe "GET /feeds/new" do - it "displays a form and submit button" do + it "displays a new feed form" do get "/feeds/new" page = last_response.body expect(page).to have_tag("form#add-feed-setup") - expect(page).to have_tag("input#submit") end end describe "POST /feeds" do context "when the feed url is valid" do let(:feed_url) { "http://example.com/" } - let(:feed) { instance_double(Feed, valid?: true) } it "adds the feed and queues it to be fetched" do - expect(AddNewFeed).to receive(:add).with(feed_url).and_return(feed) - expect(FetchFeeds).to receive(:enqueue).with([feed]) + stub_request(:get, feed_url).to_return(status: 200, body: "") - post("/feeds", feed_url:) + expect { post("/feeds", feed_url:) }.to change(Feed, :count).by(1) + end - expect(last_response.status).to be(302) - expect(URI.parse(last_response.location).path).to eq("/") + it "queues the feed to be fetched" do + stub_request(:get, feed_url).to_return(status: 200, body: "") + expect(FetchFeeds).to receive(:enqueue).with([instance_of(Feed)]) + + post("/feeds", feed_url:) end end context "when the feed url is invalid" do let(:feed_url) { "http://not-a-valid-feed.com/" } - it "adds the feed and queues it to be fetched" do - expect(AddNewFeed).to receive(:add).with(feed_url).and_return(false) - + it "does not add the feed" do + stub_request(:get, feed_url).to_return(status: 404) post("/feeds", feed_url:) page = last_response.body @@ -130,11 +124,10 @@ def params(feed, **overrides) context "when the feed url is one we already subscribe to" do let(:feed_url) { "http://example.com/" } - let(:invalid_feed) { instance_double(Feed, valid?: false) } - it "adds the feed and queues it to be fetched" do - expect(AddNewFeed) - .to receive(:add).with(feed_url).and_return(invalid_feed) + it "does not add the feed" do + create(:feed, url: feed_url) + stub_request(:get, feed_url).to_return(status: 200, body: "") post("/feeds", feed_url:) From 4ef7b1923b2a795c80f5f4d2e0a6ceb1c41a8bca Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 15:35:18 -0800 Subject: [PATCH 0473/1107] RuboCop: reduce expects in FirstRunController specs (#781) --- .rubocop_todo.yml | 1 - spec/controllers/first_run_controller_spec.rb | 7 ------- 2 files changed, 8 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b1530a98c..624b73fcb 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -249,7 +249,6 @@ RSpec/MultipleExpectations: - 'spec/commands/users/change_user_password_spec.rb' - 'spec/commands/users/complete_setup_spec.rb' - 'spec/commands/users/create_user_spec.rb' - - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/imports_controller_spec.rb' - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' diff --git a/spec/controllers/first_run_controller_spec.rb b/spec/controllers/first_run_controller_spec.rb index 25b5840e9..bf9e4465a 100644 --- a/spec/controllers/first_run_controller_spec.rb +++ b/spec/controllers/first_run_controller_spec.rb @@ -20,7 +20,6 @@ def setup page = last_response.body expect(page).to have_tag("form#password_setup") - expect(page).to have_tag("input#password") end end @@ -44,10 +43,6 @@ def setup end it "accepts confirmed passwords and redirects to next step" do - setup - user = instance_double(User, id: 1) - expect(CreateUser).to receive(:call).with("foo").and_return(user) - post "/setup/password", password: "foo", password_confirmation: "foo" expect(URI.parse(last_response.location).path).to eq("/feeds/import") @@ -75,7 +70,6 @@ def setup session = { "rack.session" => { user_id: user.id } } get "/setup/tutorial", {}, session - expect(last_response.status).to be(302) expect(URI.parse(last_response.location).path).to eq("/news") end @@ -84,7 +78,6 @@ def setup session = { "rack.session" => { user_id: user.id } } get "/", {}, session - expect(last_response.status).to be(302) expect(URI.parse(last_response.location).path).to eq("/news") end end From 440d689d58a4d012046518f4690f1c72fda56879 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 15:40:13 -0800 Subject: [PATCH 0474/1107] RuboCop: reduce expects in ImportsController specs (#782) --- .rubocop_todo.yml | 1 - spec/controllers/imports_controller_spec.rb | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 624b73fcb..e218ae5b9 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -249,7 +249,6 @@ RSpec/MultipleExpectations: - 'spec/commands/users/change_user_password_spec.rb' - 'spec/commands/users/complete_setup_spec.rb' - 'spec/commands/users/create_user_spec.rb' - - 'spec/controllers/imports_controller_spec.rb' - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api/authentication_spec.rb' diff --git a/spec/controllers/imports_controller_spec.rb b/spec/controllers/imports_controller_spec.rb index d2ea96841..9ae906fa7 100644 --- a/spec/controllers/imports_controller_spec.rb +++ b/spec/controllers/imports_controller_spec.rb @@ -11,7 +11,6 @@ page = last_response.body expect(page).to have_tag("input#opml_file") - expect(page).to have_tag("a#skip") end end @@ -23,13 +22,10 @@ ) end - it "parse OPML and starts fetching" do + it "parses OPML and starts fetching" do expect(ImportFromOpml).to receive(:import).once post "/feeds/import", "opml_file" => opml_file - - expect(last_response.status).to be(302) - expect(URI.parse(last_response.location).path).to eq("/setup/tutorial") end end end From 4236f2bacda8810558ffdba458266ff7c1979b20 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 15:44:28 -0800 Subject: [PATCH 0475/1107] RuboCop: reduce expects in SessionsController specs (#783) --- .rubocop_todo.yml | 1 - spec/controllers/sessions_controller_spec.rb | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e218ae5b9..f675bd3e8 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -249,7 +249,6 @@ RSpec/MultipleExpectations: - 'spec/commands/users/change_user_password_spec.rb' - 'spec/commands/users/complete_setup_spec.rb' - 'spec/commands/users/create_user_spec.rb' - - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api/authentication_spec.rb' - 'spec/fever_api/read_feeds_spec.rb' diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index b5e92e5a2..e80e95cf5 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -11,14 +11,12 @@ page = last_response.body expect(page).to have_tag("input#password") - expect(page).to have_tag("#login") end end describe "POST /login" do it "denies access when password is incorrect" do - expect(SignInUser).to receive(:sign_in).and_return(nil) - + create(:user) post "/login", password: "not-the-password" page = last_response.body @@ -31,8 +29,13 @@ post "/login", password: user.password expect(session[:user_id]).to eq(user.id) + end + + it "redirects to the root page" do + user = create(:user) + + post "/login", password: user.password - expect(last_response.status).to be(302) expect(URI.parse(last_response.location).path).to eq("/") end @@ -42,18 +45,20 @@ params = { password: user.password } post "/login", params, "rack.session" => { redirect_to: "/archive" } - expect(session[:redirect_to]).to be_nil expect(URI.parse(last_response.location).path).to eq("/archive") end end describe "GET /logout" do - it "clears the session and redirects" do + it "clears the session" do get "/logout", {}, "rack.session" => { userid: 1 } expect(session[:user_id]).to be_nil + end + + it "redirects to the root page" do + get "/logout", {}, "rack.session" => { userid: 1 } - expect(last_response.status).to be(302) expect(URI.parse(last_response.location).path).to eq("/") end end From 5bbb775bc4bb81d21626ee4e364dbc51625f4c5c Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 15:49:44 -0800 Subject: [PATCH 0476/1107] RuboCop: reduce expectations in StoriesController specs (#784) --- .rubocop_todo.yml | 1 - spec/controllers/stories_controller_spec.rb | 71 ++++----------------- 2 files changed, 14 insertions(+), 58 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f675bd3e8..ae1b604e5 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -249,7 +249,6 @@ RSpec/MultipleExpectations: - 'spec/commands/users/change_user_password_spec.rb' - 'spec/commands/users/complete_setup_spec.rb' - 'spec/commands/users/create_user_spec.rb' - - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api/authentication_spec.rb' - 'spec/fever_api/read_feeds_spec.rb' - 'spec/fever_api/read_groups_spec.rb' diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index 595806394..a0c066e88 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -30,7 +30,6 @@ def setup get "/news" expect(last_response.body).to include(story_one.headline) - expect(last_response.body).to include(story_one.source) end it "displays all user actions" do @@ -39,8 +38,6 @@ def setup get "/news" expect(last_response.body).to have_tag("#mark-all") - expect(last_response.body).to have_tag("#refresh") - expect(last_response.body).to have_tag("#feeds") end it "has correct footer links" do @@ -48,14 +45,11 @@ def setup get "/news" - page = last_response.body - expect(page).to have_tag("a", with: { href: "/feeds/export" }) - expect(page).to have_tag("a", with: { href: "/logout" }) + rendered = Capybara.string(last_response.body) + expect(rendered).to have_link("Export").and have_link("Logout") end it "displays a zen-like message when there are no unread stories" do - expect(StoryRepository).to receive(:unread).and_return([]) - get "/news" expect(last_response.body).to have_tag("#zen") @@ -63,114 +57,77 @@ def setup end describe "GET /archive" do - let(:read_one) { build(:story, :read) } - let(:read_two) { build(:story, :read) } - let(:stories) { [read_one, read_two].paginate } - it "displays the list of read stories with pagination" do - expect(StoryRepository).to receive(:read).and_return(stories) + create(:story, :read) get "/archive" page = last_response.body expect(page).to have_tag("#stories") - expect(page).to have_tag("div#pagination") end end describe "GET /starred" do - let(:starred_one) { build(:story, :starred) } - let(:starred_two) { build(:story, :starred) } - let(:stories) { [starred_one, starred_two].paginate } - - it "displays the list of starred stories with pagination" do - expect(StoryRepository).to receive(:starred).and_return(stories) + it "displays the list of starred stories" do + create(:story, :starred) get "/starred" page = last_response.body expect(page).to have_tag("#stories") - expect(page).to have_tag("div#pagination") end end describe "PUT /stories/:id" do it "marks a story as read when it is_read not malformed" do - expect(StoryRepository).to receive(:fetch).and_return(story_one) - expect(story_one).to receive(:save!).once - put "/stories/#{story_one.id}", { is_read: true }.to_json - expect(story_one.is_read).to be(true) + expect(story_one.reload.is_read).to be(true) end it "marks a story as read when is_read is malformed" do - expect(StoryRepository).to receive(:fetch).and_return(story_one) - expect(story_one).to receive(:save!).once - put "/stories/#{story_one.id}", { is_read: "malformed" }.to_json - expect(story_one.is_read).to be(true) + expect(story_one.reload.is_read).to be(true) end it "marks a story as keep unread when it keep_unread not malformed" do - expect(StoryRepository).to receive(:fetch).and_return(story_one) - put "/stories/#{story_one.id}", { keep_unread: false }.to_json - expect(story_one.keep_unread).to be(false) + expect(story_one.reload.keep_unread).to be(false) end it "marks a story as keep unread when keep_unread is malformed" do - expect(StoryRepository).to receive(:fetch).and_return(story_one) - put "/stories/#{story_one.id}", { keep_unread: "malformed" }.to_json - expect(story_one.keep_unread).to be(true) + expect(story_one.reload.keep_unread).to be(true) end it "marks a story as starred when is_starred is not malformed" do - expect(StoryRepository).to receive(:fetch).and_return(story_one) - put "/stories/#{story_one.id}", { is_starred: true }.to_json - expect(story_one.is_starred).to be(true) + expect(story_one.reload.is_starred).to be(true) end it "marks a story as starred when is_starred is malformed" do - expect(StoryRepository).to receive(:fetch).and_return(story_one) - put "/stories/#{story_one.id}", { is_starred: "malformed" }.to_json - expect(story_one.is_starred).to be(true) + expect(story_one.reload.is_starred).to be(true) end end describe "POST /stories/mark_all_as_read" do it "marks all unread stories as read and reload the page" do - expect(MarkAllAsRead).to receive(:call).once + stories = create_pair(:story) - post "/stories/mark_all_as_read", story_ids: ["1", "2", "3"] + post "/stories/mark_all_as_read", story_ids: stories.map(&:id) - expect(last_response.status).to be(302) - expect(URI.parse(last_response.location).path).to eq("/news") + expect(stories.map(&:reload).map(&:is_read)).to all(be(true)) end end describe "GET /feed/:feed_id" do - it "looks for a particular feed" do - expect(FeedRepository).to receive(:fetch) - .with(story_one.feed.id.to_s).and_return(story_one.feed) - expect(StoryRepository) - .to receive(:feed).with(story_one.feed.id.to_s).and_return([story_one]) - - get "/feed/#{story_one.feed.id}" - end - it "displays a list of stories" do - expect(FeedRepository).to receive(:fetch).and_return(story_one.feed) - expect(StoryRepository).to receive(:feed).and_return(stories) - get "/feed/#{story_one.feed.id}" expect(last_response.body).to have_tag("#stories") From f079e8753c2b3015c57de5946d1abab475dc85c9 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 15:54:09 -0800 Subject: [PATCH 0477/1107] RuboCop: enable HeredocDelimiterNaming (#785) --- .rubocop_todo.yml | 9 --------- app/utils/sample_story.rb | 4 ++-- spec/helpers/url_helpers_spec.rb | 16 ++++++++-------- spec/utils/opml_parser_spec.rb | 16 ++++++++-------- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index ae1b604e5..3a6bb7ec8 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -39,15 +39,6 @@ Metrics/MethodLength: - 'config/asset_pipeline.rb' - 'db/migrate/20130425222157_add_delayed_job.rb' -# Offense count: 9 -# Configuration parameters: ForbiddenDelimiters. -# ForbiddenDelimiters: (?-mix:(^|\s)(EO[A-Z]{1}|END)(\s|$)) -Naming/HeredocDelimiterNaming: - Exclude: - - 'app/utils/sample_story.rb' - - 'spec/helpers/url_helpers_spec.rb' - - 'spec/utils/opml_parser_spec.rb' - # Offense count: 2 # Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros. # NamePrefix: is_, has_, have_ diff --git a/app/utils/sample_story.rb b/app/utils/sample_story.rb index 1bde2143d..16c0bd1b4 100644 --- a/app/utils/sample_story.rb +++ b/app/utils/sample_story.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -SAMPLE_BODY = <<~EOS +SAMPLE_BODY = <<~HTML

    Tofu shoreditch intelligentsia umami, fashion axe photo booth try-hard terry richardson quinoa actually fingerstache meggings fixie. Aesthetic salvia vinyl raw denim, keffiyeh master cleanse tonx selfies mlkshk occupy twee @@ -19,7 +19,7 @@ Single-origin coffee direct trade retro biodiesel, truffaut fanny pack portland blue bottle scenester bushwick. Skateboard squid fanny pack bushwick, photo booth vice literally.

    -EOS +HTML SampleStory = Struct.new(:source, :title, :lead, :is_read, :published) do diff --git a/spec/helpers/url_helpers_spec.rb b/spec/helpers/url_helpers_spec.rb index 4c8561efd..8e8a7a5b3 100644 --- a/spec/helpers/url_helpers_spec.rb +++ b/spec/helpers/url_helpers_spec.rb @@ -18,23 +18,23 @@ end it "replaces relative urls in a, img and video tags" do - content = <<~EOS + content = <<~HTML
    tee
    - EOS + HTML result = helper.expand_absolute_urls(content, "http://oodl.io/d/") - expect(result.delete("\n")).to eq <<~EOS.delete("\n") + expect(result.delete("\n")).to eq <<~HTML.delete("\n")
    tee
    - EOS + HTML end it "handles empty body" do @@ -42,22 +42,22 @@ end it "doesn't modify tags that do not have url attributes" do - content = <<~EOS + content = <<~HTML
    - EOS + HTML result = helper.expand_absolute_urls(content, "http://oodl.io/d/") - expect(result.delete("\n")).to eq <<~EOS.delete("\n") + expect(result.delete("\n")).to eq <<~HTML.delete("\n")
    - EOS + HTML end it "leaves the url as-is if it cannot be parsed" do diff --git a/spec/utils/opml_parser_spec.rb b/spec/utils/opml_parser_spec.rb index afdf21fad..0db3f6781 100644 --- a/spec/utils/opml_parser_spec.rb +++ b/spec/utils/opml_parser_spec.rb @@ -9,7 +9,7 @@ describe "#parse_feeds" do it "returns a hash of feed details from an OPML file" do - result = parser.parse_feeds(<<-EOS) + result = parser.parse_feeds(<<-XML) @@ -22,7 +22,7 @@ xmlUrl="http://mdswanson.com/atom.xml" htmlUrl="http://mdswanson.com/"/> - EOS + XML resulted_values = result.values.flatten expect(resulted_values.size).to eq 2 @@ -35,7 +35,7 @@ end it "handles nested groups of feeds" do - result = parser.parse_feeds(<<-EOS) + result = parser.parse_feeds(<<-XML) @@ -48,7 +48,7 @@ - EOS + XML resulted_values = result.values.flatten expect(resulted_values.count).to eq 1 @@ -58,7 +58,7 @@ end it "doesn't explode when there are no feeds" do - result = parser.parse_feeds(<<-EOS) + result = parser.parse_feeds(<<-XML) @@ -67,13 +67,13 @@ - EOS + XML expect(result).to be_empty end it "handles Feedly's exported OPML (missing :title)" do - result = parser.parse_feeds(<<-EOS) + result = parser.parse_feeds(<<-XML) @@ -84,7 +84,7 @@ xmlUrl="http://feeds.feedburner.com/foobar" htmlUrl="http://www.example.org/"/> - EOS + XML resulted_values = result.values.flatten expect(resulted_values.count).to eq 1 From f2cf11e34effc3c1fad78cd7c98048902a3e8982 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 15:58:17 -0800 Subject: [PATCH 0478/1107] RuboCop: enable BeforeAfterAll cop (#786) --- .rubocop_todo.yml | 5 ----- spec/integration/feed_importing_spec.rb | 6 +++--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3a6bb7ec8..2645be3e1 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -59,11 +59,6 @@ RSpec/Be: Exclude: - 'spec/commands/feeds/import_from_opml_spec.rb' -# Offense count: 3 -RSpec/BeforeAfterAll: - Exclude: - - 'spec/integration/feed_importing_spec.rb' - # Offense count: 18 # Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without diff --git a/spec/integration/feed_importing_spec.rb b/spec/integration/feed_importing_spec.rb index d3b09acea..291d93dda 100644 --- a/spec/integration/feed_importing_spec.rb +++ b/spec/integration/feed_importing_spec.rb @@ -9,7 +9,7 @@ app_require "tasks/fetch_feed" describe "Feed importing" do - before(:all) { @server = FeedServer.new } + before { @server = FeedServer.new } let(:feed) do Feed.create( @@ -20,7 +20,7 @@ end describe "Valid feed" do - before(:all) do + before do # articles older than 3 days are ignored, so freeze time within # applicable range of the stories in the sample feed Timecop.freeze Time.parse("2014-08-15T17:30:00Z") @@ -57,7 +57,7 @@ end describe "Feed with incorrect pubdates" do - before(:all) { Timecop.freeze Time.parse("2014-08-12T17:30:00Z") } + before { Timecop.freeze Time.parse("2014-08-12T17:30:00Z") } context "has been fetched before" do it "imports all new stories" do From da3413b714ae5474923a5f91fd0679b11f61a610 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 16:02:48 -0800 Subject: [PATCH 0479/1107] RuboCop: enable RSpec/DescribedClass cop (#787) --- .rubocop_todo.yml | 43 ------- spec/commands/feeds/add_new_feed_spec.rb | 8 +- spec/commands/feeds/export_to_opml_spec.rb | 6 +- spec/commands/find_new_stories_spec.rb | 8 +- .../commands/stories/mark_all_as_read_spec.rb | 2 +- spec/commands/stories/mark_as_read_spec.rb | 2 +- spec/commands/stories/mark_as_starred_spec.rb | 2 +- spec/commands/stories/mark_as_unread_spec.rb | 2 +- .../stories/mark_as_unstarred_spec.rb | 2 +- .../stories/mark_feed_as_read_spec.rb | 2 +- .../users/change_user_password_spec.rb | 4 +- spec/commands/users/complete_setup_spec.rb | 2 +- spec/commands/users/sign_in_user_spec.rb | 4 +- spec/fever_api/authentication_spec.rb | 2 +- spec/fever_api/read_favicons_spec.rb | 2 +- spec/fever_api/read_feeds_groups_spec.rb | 2 +- spec/fever_api/read_feeds_spec.rb | 2 +- spec/fever_api/read_groups_spec.rb | 2 +- spec/fever_api/read_items_spec.rb | 2 +- spec/fever_api/read_links_spec.rb | 2 +- spec/fever_api/sync_saved_item_ids_spec.rb | 2 +- spec/fever_api/sync_unread_item_ids_spec.rb | 2 +- spec/fever_api/write_mark_feed_spec.rb | 2 +- spec/fever_api/write_mark_group_spec.rb | 2 +- spec/fever_api/write_mark_item_spec.rb | 14 +- spec/models/group_spec.rb | 2 +- spec/repositories/feed_repository_spec.rb | 29 +++-- spec/repositories/group_repository_spec.rb | 2 +- spec/repositories/story_repository_spec.rb | 120 +++++++++--------- spec/repositories/user_repository_spec.rb | 16 +-- spec/tasks/change_password_spec.rb | 4 +- spec/tasks/fetch_feed_spec.rb | 26 ++-- spec/tasks/fetch_feeds_spec.rb | 9 +- spec/tasks/remove_old_stories_spec.rb | 18 +-- spec/utils/feed_discovery_spec.rb | 8 +- spec/utils/opml_parser_spec.rb | 2 +- 36 files changed, 161 insertions(+), 198 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 2645be3e1..eead9e7dd 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -80,49 +80,6 @@ RSpec/DescribeClass: - 'spec/integration/feed_importing_spec.rb' - 'spec/utils/i18n_support_spec.rb' -# Offense count: 149 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: SkipBlocks, EnforcedStyle. -# SupportedStyles: described_class, explicit -RSpec/DescribedClass: - Exclude: - - 'spec/commands/feeds/add_new_feed_spec.rb' - - 'spec/commands/feeds/export_to_opml_spec.rb' - - 'spec/commands/find_new_stories_spec.rb' - - 'spec/commands/stories/mark_all_as_read_spec.rb' - - 'spec/commands/stories/mark_as_read_spec.rb' - - 'spec/commands/stories/mark_as_starred_spec.rb' - - 'spec/commands/stories/mark_as_unread_spec.rb' - - 'spec/commands/stories/mark_as_unstarred_spec.rb' - - 'spec/commands/stories/mark_feed_as_read_spec.rb' - - 'spec/commands/users/change_user_password_spec.rb' - - 'spec/commands/users/complete_setup_spec.rb' - - 'spec/commands/users/create_user_spec.rb' - - 'spec/commands/users/sign_in_user_spec.rb' - - 'spec/fever_api/authentication_spec.rb' - - 'spec/fever_api/read_favicons_spec.rb' - - 'spec/fever_api/read_feeds_groups_spec.rb' - - 'spec/fever_api/read_feeds_spec.rb' - - 'spec/fever_api/read_groups_spec.rb' - - 'spec/fever_api/read_items_spec.rb' - - 'spec/fever_api/read_links_spec.rb' - - 'spec/fever_api/sync_saved_item_ids_spec.rb' - - 'spec/fever_api/sync_unread_item_ids_spec.rb' - - 'spec/fever_api/write_mark_feed_spec.rb' - - 'spec/fever_api/write_mark_group_spec.rb' - - 'spec/fever_api/write_mark_item_spec.rb' - - 'spec/models/group_spec.rb' - - 'spec/repositories/feed_repository_spec.rb' - - 'spec/repositories/group_repository_spec.rb' - - 'spec/repositories/story_repository_spec.rb' - - 'spec/repositories/user_repository_spec.rb' - - 'spec/tasks/change_password_spec.rb' - - 'spec/tasks/fetch_feed_spec.rb' - - 'spec/tasks/fetch_feeds_spec.rb' - - 'spec/tasks/remove_old_stories_spec.rb' - - 'spec/utils/feed_discovery_spec.rb' - - 'spec/utils/opml_parser_spec.rb' - # Offense count: 63 # Configuration parameters: Max, CountAsOne. RSpec/ExampleLength: diff --git a/spec/commands/feeds/add_new_feed_spec.rb b/spec/commands/feeds/add_new_feed_spec.rb index 8893f5434..dc2df8187 100644 --- a/spec/commands/feeds/add_new_feed_spec.rb +++ b/spec/commands/feeds/add_new_feed_spec.rb @@ -10,7 +10,7 @@ let(:discoverer) { double(discover: false) } it "returns false if cant discover any feeds" do - result = AddNewFeed.add("http://not-a-feed.com", discoverer) + result = described_class.add("http://not-a-feed.com", discoverer) expect(result).to be(false) end @@ -26,7 +26,7 @@ it "parses and creates the feed if discovered" do expect(repo).to receive(:create).and_return(feed) - result = AddNewFeed.add("http://feed.com", discoverer, repo) + result = described_class.add("http://feed.com", discoverer, repo) expect(result).to be feed end @@ -42,7 +42,7 @@ it "deletes the script tag from the title" do allow(repo).to receive(:create) - AddNewFeed.add("http://feed.com", discoverer, repo) + described_class.add("http://feed.com", discoverer, repo) expect(repo).to have_received(:create).with(include(name: "foobar")) end @@ -54,7 +54,7 @@ result = instance_double(Feedjira::Parser::RSS, title: nil, feed_url:) discoverer = instance_double(FeedDiscovery, discover: result) - expect { AddNewFeed.add(feed_url, discoverer) } + expect { described_class.add(feed_url, discoverer) } .to change(Feed, :count).by(1) expect(Feed.last.name).to eq(feed_url) diff --git a/spec/commands/feeds/export_to_opml_spec.rb b/spec/commands/feeds/export_to_opml_spec.rb index 1ed1ec622..01c89a82e 100644 --- a/spec/commands/feeds/export_to_opml_spec.rb +++ b/spec/commands/feeds/export_to_opml_spec.rb @@ -11,7 +11,7 @@ let(:feeds) { [feed_one, feed_two] } it "returns OPML XML" do - result = ExportToOpml.new(feeds).to_xml + result = described_class.new(feeds).to_xml outlines = Nokogiri.XML(result).xpath("//body//outline") expect(outlines.size).to eq(2) @@ -22,14 +22,14 @@ end it "handles empty feeds" do - result = ExportToOpml.new([]).to_xml + result = described_class.new([]).to_xml outlines = Nokogiri.XML(result).xpath("//body//outline") expect(outlines).to be_empty end it "has a proper title" do - result = ExportToOpml.new(feeds).to_xml + result = described_class.new(feeds).to_xml title = Nokogiri.XML(result).xpath("//head//title").first expect(title.content).to eq "Feeds from Stringer" diff --git a/spec/commands/find_new_stories_spec.rb b/spec/commands/find_new_stories_spec.rb index 5f2f45c63..903c3447f 100644 --- a/spec/commands/find_new_stories_spec.rb +++ b/spec/commands/find_new_stories_spec.rb @@ -15,7 +15,7 @@ story2 = double(published: nil, id: "story2") feed = double(entries: [story1, story2]) - result = FindNewStories.new(feed, 1, Time.new(2013, 1, 2)).new_stories + result = described_class.new(feed, 1, Time.new(2013, 1, 2)).new_stories expect(result).to be_empty end end @@ -31,7 +31,7 @@ allow(StoryRepository) .to receive(:exists?).with("story2", 1).and_return(false) - result = FindNewStories.new(feed, 1, Time.new(2013, 1, 2)).new_stories + result = described_class.new(feed, 1, Time.new(2013, 1, 2)).new_stories expect(result).to eq [story2] end end @@ -41,7 +41,7 @@ old_story = double(published: nil, id: "old-story") feed = double(last_modified: nil, entries: [new_story, old_story]) - result = FindNewStories.new( + result = described_class.new( feed, 1, Time.new(2013, 1, 3), @@ -66,7 +66,7 @@ entries: new_stories + stories_older_than_3_days ) - result = FindNewStories.new(feed, 1, nil, nil).new_stories + result = described_class.new(feed, 1, nil, nil).new_stories expect(result).not_to include(stories_older_than_3_days) end end diff --git a/spec/commands/stories/mark_all_as_read_spec.rb b/spec/commands/stories/mark_all_as_read_spec.rb index b9880759e..e9555a794 100644 --- a/spec/commands/stories/mark_all_as_read_spec.rb +++ b/spec/commands/stories/mark_all_as_read_spec.rb @@ -12,7 +12,7 @@ it "marks all stories as read" do expect(stories).to receive(:update_all).with(is_read: true) - MarkAllAsRead.call([1, 2], repo) + described_class.call([1, 2], repo) end end end diff --git a/spec/commands/stories/mark_as_read_spec.rb b/spec/commands/stories/mark_as_read_spec.rb index 603e99e13..814aac756 100644 --- a/spec/commands/stories/mark_as_read_spec.rb +++ b/spec/commands/stories/mark_as_read_spec.rb @@ -9,7 +9,7 @@ let(:story) { create(:story, is_read: false) } it "marks a story as read" do - expect { MarkAsRead.new(story.id).mark_as_read } + expect { described_class.new(story.id).mark_as_read } .to change { Story.find(story.id).is_read } .to(true) end diff --git a/spec/commands/stories/mark_as_starred_spec.rb b/spec/commands/stories/mark_as_starred_spec.rb index 150d78fe1..c5ece86d4 100644 --- a/spec/commands/stories/mark_as_starred_spec.rb +++ b/spec/commands/stories/mark_as_starred_spec.rb @@ -9,7 +9,7 @@ let(:story) { create(:story, is_starred: false) } it "marks a story as starred" do - expect { MarkAsStarred.new(story.id).mark_as_starred } + expect { described_class.new(story.id).mark_as_starred } .to change { Story.find(story.id).is_starred } .to(true) end diff --git a/spec/commands/stories/mark_as_unread_spec.rb b/spec/commands/stories/mark_as_unread_spec.rb index c5d72427b..ed113691c 100644 --- a/spec/commands/stories/mark_as_unread_spec.rb +++ b/spec/commands/stories/mark_as_unread_spec.rb @@ -9,7 +9,7 @@ let(:story) { create(:story, is_read: true) } it "marks a story as unread" do - expect { MarkAsUnread.new(story.id).mark_as_unread } + expect { described_class.new(story.id).mark_as_unread } .to change { Story.find(story.id).is_read } .to(false) end diff --git a/spec/commands/stories/mark_as_unstarred_spec.rb b/spec/commands/stories/mark_as_unstarred_spec.rb index 5802c752f..a0dbcd167 100644 --- a/spec/commands/stories/mark_as_unstarred_spec.rb +++ b/spec/commands/stories/mark_as_unstarred_spec.rb @@ -9,7 +9,7 @@ let(:story) { create(:story, is_starred: true) } it "marks a story as unstarred" do - expect { MarkAsUnstarred.new(story.id).mark_as_unstarred } + expect { described_class.new(story.id).mark_as_unstarred } .to change { Story.find(story.id).is_starred } .to(false) end diff --git a/spec/commands/stories/mark_feed_as_read_spec.rb b/spec/commands/stories/mark_feed_as_read_spec.rb index 72d586e87..63a7dec3b 100644 --- a/spec/commands/stories/mark_feed_as_read_spec.rb +++ b/spec/commands/stories/mark_feed_as_read_spec.rb @@ -10,7 +10,7 @@ let(:repo) { double(fetch_unread_for_feed_by_timestamp: stories) } it "marks feed 1 as read" do - command = MarkFeedAsRead.new(1, Time.now.to_i, repo) + command = described_class.new(1, Time.now.to_i, repo) expect(stories).to receive(:update_all).with(is_read: true) command.mark_feed_as_read end diff --git a/spec/commands/users/change_user_password_spec.rb b/spec/commands/users/change_user_password_spec.rb index 61a208e76..f109d666b 100644 --- a/spec/commands/users/change_user_password_spec.rb +++ b/spec/commands/users/change_user_password_spec.rb @@ -17,7 +17,7 @@ expect(repo).to receive(:first).and_return(user) expect(repo).to receive(:save) - command = ChangeUserPassword.new(repo) + command = described_class.new(repo) result = command.change_user_password(new_password) expect(BCrypt::Password.new(result.password_digest)).to eq new_password @@ -27,7 +27,7 @@ expect(repo).to receive(:first).and_return(user) expect(repo).to receive(:save) - command = ChangeUserPassword.new(repo) + command = described_class.new(repo) result = command.change_user_password(new_password) expect(result.api_key).to eq ApiKey.compute(new_password) diff --git a/spec/commands/users/complete_setup_spec.rb b/spec/commands/users/complete_setup_spec.rb index 5c1ad2a88..6e07fc86d 100644 --- a/spec/commands/users/complete_setup_spec.rb +++ b/spec/commands/users/complete_setup_spec.rb @@ -10,7 +10,7 @@ it "marks setup as complete" do expect(user).to receive(:save).once - result = CompleteSetup.complete(user) + result = described_class.complete(user) expect(result.setup_complete).to be(true) end end diff --git a/spec/commands/users/sign_in_user_spec.rb b/spec/commands/users/sign_in_user_spec.rb index 7132a70e3..e5dbc0137 100644 --- a/spec/commands/users/sign_in_user_spec.rb +++ b/spec/commands/users/sign_in_user_spec.rb @@ -14,13 +14,13 @@ describe "#sign_in" do it "returns the user if the password is valid" do - result = SignInUser.sign_in(valid_password, repo) + result = described_class.sign_in(valid_password, repo) expect(result.id).to eq 1 end it "returns nil if password is invalid" do - result = SignInUser.sign_in("not-the-pw", repo) + result = described_class.sign_in("not-the-pw", repo) expect(result).to be_nil end diff --git a/spec/fever_api/authentication_spec.rb b/spec/fever_api/authentication_spec.rb index 4cfd295dc..18cca061f 100644 --- a/spec/fever_api/authentication_spec.rb +++ b/spec/fever_api/authentication_spec.rb @@ -8,7 +8,7 @@ it "returns a hash with keys :auth and :last_refreshed_on_time" do fake_clock = double("clock") expect(fake_clock).to receive(:now).and_return(1234567890) - result = FeverAPI::Authentication.new(clock: fake_clock).call(double) + result = described_class.new(clock: fake_clock).call(double) expect(result).to eq(auth: 1, last_refreshed_on_time: 1234567890) end end diff --git a/spec/fever_api/read_favicons_spec.rb b/spec/fever_api/read_favicons_spec.rb index 19ce8a94b..26f5afbda 100644 --- a/spec/fever_api/read_favicons_spec.rb +++ b/spec/fever_api/read_favicons_spec.rb @@ -5,7 +5,7 @@ app_require "fever_api/read_favicons" describe FeverAPI::ReadFavicons do - subject { FeverAPI::ReadFavicons.new } + subject { described_class.new } it "returns a fixed icon list if requested" do expect(subject.call("favicons" => nil)).to eq( diff --git a/spec/fever_api/read_feeds_groups_spec.rb b/spec/fever_api/read_feeds_groups_spec.rb index e4004fd20..ce98c876f 100644 --- a/spec/fever_api/read_feeds_groups_spec.rb +++ b/spec/fever_api/read_feeds_groups_spec.rb @@ -9,7 +9,7 @@ let(:feeds) { feed_ids.map { |id| double("feed", id:, group_id: 1) } } let(:feed_repository) { double("repo") } - subject { FeverAPI::ReadFeedsGroups.new(feed_repository:) } + subject { described_class.new(feed_repository:) } it "returns a list of groups requested through feeds" do allow(feed_repository) diff --git a/spec/fever_api/read_feeds_spec.rb b/spec/fever_api/read_feeds_spec.rb index f429153d4..a49766d98 100644 --- a/spec/fever_api/read_feeds_spec.rb +++ b/spec/fever_api/read_feeds_spec.rb @@ -11,7 +11,7 @@ end let(:feed_repository) { double("repo") } - subject { FeverAPI::ReadFeeds.new(feed_repository:) } + subject { described_class.new(feed_repository:) } it "returns a list of feeds" do expect(feed_repository).to receive(:list).and_return(feeds) diff --git a/spec/fever_api/read_groups_spec.rb b/spec/fever_api/read_groups_spec.rb index d36002995..176429b41 100644 --- a/spec/fever_api/read_groups_spec.rb +++ b/spec/fever_api/read_groups_spec.rb @@ -11,7 +11,7 @@ end let(:group_repository) { double("repo") } - subject { FeverAPI::ReadGroups.new(group_repository:) } + subject { described_class.new(group_repository:) } it "returns a group list if requested" do expect(group_repository).to receive(:list).and_return([group1, group2]) diff --git a/spec/fever_api/read_items_spec.rb b/spec/fever_api/read_items_spec.rb index caa375f44..cd71d5511 100644 --- a/spec/fever_api/read_items_spec.rb +++ b/spec/fever_api/read_items_spec.rb @@ -7,7 +7,7 @@ describe FeverAPI::ReadItems do let(:story_repository) { double("repo") } - subject { FeverAPI::ReadItems.new(story_repository:) } + subject { described_class.new(story_repository:) } it "returns a list of unread items including total count" do stories = [ diff --git a/spec/fever_api/read_links_spec.rb b/spec/fever_api/read_links_spec.rb index 3f279179d..837c2ac38 100644 --- a/spec/fever_api/read_links_spec.rb +++ b/spec/fever_api/read_links_spec.rb @@ -5,7 +5,7 @@ app_require "fever_api/read_links" describe FeverAPI::ReadLinks do - subject { FeverAPI::ReadLinks.new } + subject { described_class.new } it "returns a fixed link list if requested" do expect(subject.call("links" => nil)).to eq(links: []) diff --git a/spec/fever_api/sync_saved_item_ids_spec.rb b/spec/fever_api/sync_saved_item_ids_spec.rb index 6acfe5e0a..daef8ce43 100644 --- a/spec/fever_api/sync_saved_item_ids_spec.rb +++ b/spec/fever_api/sync_saved_item_ids_spec.rb @@ -9,7 +9,7 @@ let(:stories) { story_ids.map { |id| double("story", id:) } } let(:story_repository) { double("repo") } - subject { FeverAPI::SyncSavedItemIds.new(story_repository:) } + subject { described_class.new(story_repository:) } it "returns a list of starred items if requested" do expect(story_repository).to receive(:all_starred).and_return(stories) diff --git a/spec/fever_api/sync_unread_item_ids_spec.rb b/spec/fever_api/sync_unread_item_ids_spec.rb index f84c22ac4..f46b7ff39 100644 --- a/spec/fever_api/sync_unread_item_ids_spec.rb +++ b/spec/fever_api/sync_unread_item_ids_spec.rb @@ -9,7 +9,7 @@ let(:stories) { story_ids.map { |id| double("story", id:) } } let(:story_repository) { double("repo") } - subject { FeverAPI::SyncUnreadItemIds.new(story_repository:) } + subject { described_class.new(story_repository:) } it "returns a list of unread items if requested" do expect(story_repository).to receive(:unread).and_return(stories) diff --git a/spec/fever_api/write_mark_feed_spec.rb b/spec/fever_api/write_mark_feed_spec.rb index 2ff1e4546..ee7c13842 100644 --- a/spec/fever_api/write_mark_feed_spec.rb +++ b/spec/fever_api/write_mark_feed_spec.rb @@ -8,7 +8,7 @@ let(:feed_marker) { double("feed marker") } let(:marker_class) { double("marker class") } - subject { FeverAPI::WriteMarkFeed.new(marker_class:) } + subject { described_class.new(marker_class:) } it "instantiates a feed marker and calls mark_feed_as_read if requested" do expect(marker_class) diff --git a/spec/fever_api/write_mark_group_spec.rb b/spec/fever_api/write_mark_group_spec.rb index 0713f19c4..16a955cfa 100644 --- a/spec/fever_api/write_mark_group_spec.rb +++ b/spec/fever_api/write_mark_group_spec.rb @@ -8,7 +8,7 @@ let(:group_marker) { double("group marker") } let(:marker_class) { double("marker class") } - subject { FeverAPI::WriteMarkGroup.new(marker_class:) } + subject { described_class.new(marker_class:) } it "instantiates a group marker and calls mark_group_as_read if requested" do expect(marker_class) diff --git a/spec/fever_api/write_mark_item_spec.rb b/spec/fever_api/write_mark_item_spec.rb index 3edb48802..0bd98cf06 100644 --- a/spec/fever_api/write_mark_item_spec.rb +++ b/spec/fever_api/write_mark_item_spec.rb @@ -9,7 +9,7 @@ let(:marker_class) { double("marker class") } describe "as read" do - subject { FeverAPI::WriteMarkItem.new(read_marker_class: marker_class) } + subject { described_class.new(read_marker_class: marker_class) } it "calls mark_item_as_read if requested" do expect(marker_class).to receive(:new).with(5).and_return(item_marker) @@ -19,9 +19,7 @@ end describe "as unread" do - subject do - FeverAPI::WriteMarkItem.new(unread_marker_class: marker_class) - end + subject { described_class.new(unread_marker_class: marker_class) } it "calls mark_item_as_unread if requested" do expect(marker_class).to receive(:new).with(5).and_return(item_marker) @@ -31,9 +29,7 @@ end describe "as starred" do - subject do - FeverAPI::WriteMarkItem.new(starred_marker_class: marker_class) - end + subject { described_class.new(starred_marker_class: marker_class) } it "calls mark_item_as_starred if requested" do expect(marker_class).to receive(:new).with(5).and_return(item_marker) @@ -43,9 +39,7 @@ end describe "as unstarred" do - subject do - FeverAPI::WriteMarkItem.new(unstarred_marker_class: marker_class) - end + subject { described_class.new(unstarred_marker_class: marker_class) } it "calls marks_item_as_unstarred if requested" do expect(marker_class).to receive(:new).with(5).and_return(item_marker) diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 6556e076b..e34ac3363 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Group do describe "#as_fever_json" do it "returns a hash of the group in fever format" do - group = Group.new(id: 5, name: "wat group") + group = described_class.new(id: 5, name: "wat group") expect(group.as_fever_json).to eq(id: 5, title: "wat group") end diff --git a/spec/repositories/feed_repository_spec.rb b/spec/repositories/feed_repository_spec.rb index 1bd31867c..f9da37222 100644 --- a/spec/repositories/feed_repository_spec.rb +++ b/spec/repositories/feed_repository_spec.rb @@ -11,13 +11,13 @@ it "finds by id" do expect(Feed).to receive(:find).with(feed.id).and_return(feed) - FeedRepository.fetch(feed.id) + described_class.fetch(feed.id) end it "returns found feed" do allow(Feed).to receive(:find).with(feed.id).and_return(feed) - result = FeedRepository.fetch(feed.id) + result = described_class.fetch(feed.id) expect(result).to eq feed end @@ -27,13 +27,14 @@ it "finds all feeds by id" do feeds = create_pair(:feed) - expect(FeedRepository.fetch_by_ids(feeds.map(&:id))).to match_array(feeds) + expect(described_class.fetch_by_ids(feeds.map(&:id))) + .to match_array(feeds) end it "does not find other feeds" do feed1, = create_pair(:feed) - expect(FeedRepository.fetch_by_ids(feed1.id)).to eq([feed1]) + expect(described_class.fetch_by_ids(feed1.id)).to eq([feed1]) end end @@ -41,7 +42,7 @@ it "saves the name and url" do feed = Feed.new - FeedRepository.update_feed(feed, "Test Feed", "example.com/feed") + described_class.update_feed(feed, "Test Feed", "example.com/feed") expect(feed.name).to eq "Test Feed" expect(feed.url).to eq "example.com/feed" @@ -54,7 +55,7 @@ it "saves the last_fetched timestamp" do feed = Feed.new - FeedRepository.update_last_fetched(feed, timestamp) + described_class.update_last_fetched(feed, timestamp) expect(feed.last_fetched).to eq timestamp end @@ -64,7 +65,7 @@ it "rejects weird timestamps" do feed = Feed.new(last_fetched: timestamp) - FeedRepository.update_last_fetched(feed, weird_timestamp) + described_class.update_last_fetched(feed, weird_timestamp) expect(feed.last_fetched).to eq timestamp end @@ -72,7 +73,7 @@ it "doesn't update if timestamp is nil" do feed = Feed.new(last_fetched: timestamp) - FeedRepository.update_last_fetched(feed, nil) + described_class.update_last_fetched(feed, nil) expect(feed.last_fetched).to eq timestamp end @@ -81,7 +82,7 @@ feed = Feed.new(last_fetched: timestamp) one_week_ago = timestamp - 1.week - FeedRepository.update_last_fetched(feed, one_week_ago) + described_class.update_last_fetched(feed, one_week_ago) expect(feed.last_fetched).to eq timestamp end @@ -91,7 +92,7 @@ it "deletes the feed by id" do feed = create(:feed) - FeedRepository.delete(feed.id) + described_class.delete(feed.id) expect(Feed.unscoped.find_by(id: feed.id)).to be_nil end @@ -99,7 +100,7 @@ it "does not delete other feeds" do feed1, feed2 = create_pair(:feed) - FeedRepository.delete(feed1.id) + described_class.delete(feed1.id) expect(Feed.unscoped.find_by(id: feed2.id)).to eq(feed2) end @@ -112,7 +113,7 @@ feed3 = create(:feed, name: "Zooby") feed4 = create(:feed, name: "zabby") - expect(FeedRepository.list).to eq([feed2, feed1, feed4, feed3]) + expect(described_class.list).to eq([feed2, feed1, feed4, feed3]) end end @@ -121,13 +122,13 @@ feed1 = create(:feed, group_id: 5) feed2 = create(:feed, group_id: 6) - expect(FeedRepository.in_group).to match_array([feed1, feed2]) + expect(described_class.in_group).to match_array([feed1, feed2]) end it "does not return feeds that are not in a group" do create_pair(:feed) - expect(FeedRepository.in_group).to be_empty + expect(described_class.in_group).to be_empty end end end diff --git a/spec/repositories/group_repository_spec.rb b/spec/repositories/group_repository_spec.rb index 2f6a5a827..48c7e600b 100644 --- a/spec/repositories/group_repository_spec.rb +++ b/spec/repositories/group_repository_spec.rb @@ -14,7 +14,7 @@ group4 = create(:group, name: "Babba") expected_groups = [group4, group3, group1, group2] - expect(GroupRepository.list).to eq(expected_groups) + expect(described_class.list).to eq(expected_groups) end end end diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index b3c806901..cc50b3046 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -17,29 +17,29 @@ title: "", content: "" ).as_null_object - expect(StoryRepository) + expect(described_class) .to receive(:normalize_url).with(entry.url, feed.url) - StoryRepository.add(entry, feed) + described_class.add(entry, feed) end it "deletes line and paragraph separator characters from titles" do entry = double(title: "n\u2028\u2029", content: "").as_null_object - allow(StoryRepository).to receive(:normalize_url) + allow(described_class).to receive(:normalize_url) expect(Story).to receive(:create).with(hash_including(title: "n")) - StoryRepository.add(entry, feed) + described_class.add(entry, feed) end it "deletes script tags from titles" do entry = double(title: "n", content: "") .as_null_object - allow(StoryRepository).to receive(:normalize_url) + allow(described_class).to receive(:normalize_url) expect(Story).to receive(:create).with(hash_including(title: "n")) - StoryRepository.add(entry, feed) + described_class.add(entry, feed) end it "sets the enclosure url when present" do @@ -50,11 +50,11 @@ summary: "", content: "" ).as_null_object - allow(StoryRepository).to receive(:normalize_url) + allow(described_class).to receive(:normalize_url) expect(Story).to receive(:create).with(hash_including(enclosure_url: "http://example.com/audio.mp3")) - StoryRepository.add(entry, feed) + described_class.add(entry, feed) end it "does not set the enclosure url when not present" do @@ -64,11 +64,11 @@ summary: "", content: "" ).as_null_object - allow(StoryRepository).to receive(:normalize_url) + allow(described_class).to receive(:normalize_url) expect(Story).to receive(:create).with(hash_including(enclosure_url: nil)) - StoryRepository.add(entry, feed) + described_class.add(entry, feed) end end @@ -76,7 +76,7 @@ it "finds the story by id" do story = create(:story) - expect(StoryRepository.fetch(story.id)).to eq(story) + expect(described_class.fetch(story.id)).to eq(story) end end @@ -86,7 +86,7 @@ story2 = create(:story) expected_stories = [story1, story2] - actual_stories = StoryRepository.fetch_by_ids(expected_stories.map(&:id)) + actual_stories = described_class.fetch_by_ids(expected_stories.map(&:id)) expect(actual_stories).to match_array(expected_stories) end @@ -96,7 +96,7 @@ it "returns unread stories from before the timestamp" do story = create(:story, created_at: 1.week.ago, is_read: false) - actual_stories = StoryRepository.fetch_unread_by_timestamp(4.days.ago) + actual_stories = described_class.fetch_unread_by_timestamp(4.days.ago) expect(actual_stories).to eq([story]) end @@ -104,7 +104,7 @@ it "does not return unread stories from after the timestamp" do create(:story, created_at: 3.days.ago, is_read: false) - actual_stories = StoryRepository.fetch_unread_by_timestamp(4.days.ago) + actual_stories = described_class.fetch_unread_by_timestamp(4.days.ago) expect(actual_stories).to be_empty end @@ -112,7 +112,7 @@ it "does not return read stories from before the timestamp" do create(:story, created_at: 1.week.ago, is_read: true) - actual_stories = StoryRepository.fetch_unread_by_timestamp(4.days.ago) + actual_stories = described_class.fetch_unread_by_timestamp(4.days.ago) expect(actual_stories).to be_empty end @@ -124,7 +124,7 @@ story = create(:story, :unread, feed:, created_at: 5.minutes.ago) time = Time.now - stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, 52) + stories = described_class.fetch_unread_by_timestamp_and_group(time, 52) expect(stories).to eq([story]) end @@ -134,7 +134,7 @@ create(:story, feed:, created_at: 5.minutes.ago) time = Time.now - stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, 52) + stories = described_class.fetch_unread_by_timestamp_and_group(time, 52) expect(stories).to be_empty end @@ -144,7 +144,7 @@ create(:story, :unread, feed:, created_at: 5.minutes.ago) time = 6.minutes.ago - stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, 52) + stories = described_class.fetch_unread_by_timestamp_and_group(time, 52) expect(stories).to be_empty end @@ -154,7 +154,7 @@ create(:story, :unread, feed:, created_at: 5.minutes.ago) time = Time.now - stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, 55) + stories = described_class.fetch_unread_by_timestamp_and_group(time, 55) expect(stories).to be_empty end @@ -164,7 +164,7 @@ create(:story, :unread, feed:, created_at: 5.minutes.ago) time = Time.now - stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, 52) + stories = described_class.fetch_unread_by_timestamp_and_group(time, 52) expect(stories).to be_empty end @@ -174,7 +174,7 @@ story = create(:story, :unread, feed:, created_at: 5.minutes.ago) time = Time.now - stories = StoryRepository.fetch_unread_by_timestamp_and_group(time, nil) + stories = described_class.fetch_unread_by_timestamp_and_group(time, nil) expect(stories).to eq([story]) end @@ -187,7 +187,7 @@ time = 4.minutes.ago stories = - StoryRepository.fetch_unread_for_feed_by_timestamp(feed.id, time) + described_class.fetch_unread_for_feed_by_timestamp(feed.id, time) expect(stories).to eq([story]) end @@ -198,7 +198,7 @@ timestamp = Integer(4.minutes.ago).to_s stories = - StoryRepository.fetch_unread_for_feed_by_timestamp(feed.id, timestamp) + described_class.fetch_unread_for_feed_by_timestamp(feed.id, timestamp) expect(stories).to eq([story]) end @@ -209,7 +209,7 @@ time = 4.minutes.ago stories = - StoryRepository.fetch_unread_for_feed_by_timestamp(feed.id, time) + described_class.fetch_unread_for_feed_by_timestamp(feed.id, time) expect(stories).to be_empty end @@ -220,7 +220,7 @@ time = 6.minutes.ago stories = - StoryRepository.fetch_unread_for_feed_by_timestamp(feed.id, time) + described_class.fetch_unread_for_feed_by_timestamp(feed.id, time) expect(stories).to be_empty end @@ -231,7 +231,7 @@ time = 4.minutes.ago stories = - StoryRepository.fetch_unread_for_feed_by_timestamp(feed.id, time) + described_class.fetch_unread_for_feed_by_timestamp(feed.id, time) expect(stories).to be_empty end @@ -242,14 +242,14 @@ story1 = create(:story, :unread, published: 5.minutes.ago) story2 = create(:story, :unread, published: 4.minutes.ago) - expect(StoryRepository.unread).to eq([story2, story1]) + expect(described_class.unread).to eq([story2, story1]) end it "does not return read stories" do create(:story, published: 5.minutes.ago) create(:story, published: 4.minutes.ago) - expect(StoryRepository.unread).to be_empty + expect(described_class.unread).to be_empty end end @@ -258,21 +258,21 @@ story1 = create(:story, :unread) story2 = create(:story, :unread) - expect(StoryRepository.unread_since_id(story1.id)).to eq([story2]) + expect(described_class.unread_since_id(story1.id)).to eq([story2]) end it "does not return read stories with id greater than given id" do story1 = create(:story, :unread) create(:story) - expect(StoryRepository.unread_since_id(story1.id)).to be_empty + expect(described_class.unread_since_id(story1.id)).to be_empty end it "does not return unread stories with id less than given id" do create(:story, :unread) story2 = create(:story, :unread) - expect(StoryRepository.unread_since_id(story2.id)).to be_empty + expect(described_class.unread_since_id(story2.id)).to be_empty end end @@ -281,7 +281,7 @@ feed = create(:feed) story = create(:story, feed:) - expect(StoryRepository.feed(feed.id)).to eq([story]) + expect(described_class.feed(feed.id)).to eq([story]) end it "sorts stories by published" do @@ -289,14 +289,14 @@ story1 = create(:story, feed:, published: 1.day.ago) story2 = create(:story, feed:, published: 1.hour.ago) - expect(StoryRepository.feed(feed.id)).to eq([story2, story1]) + expect(described_class.feed(feed.id)).to eq([story2, story1]) end it "does not return stories for other feeds" do feed = create(:feed) create(:story) - expect(StoryRepository.feed(feed.id)).to be_empty + expect(described_class.feed(feed.id)).to be_empty end end @@ -304,28 +304,28 @@ it "returns read stories" do story = create(:story, :read) - expect(StoryRepository.read).to eq([story]) + expect(described_class.read).to eq([story]) end it "sorts stories by published" do story1 = create(:story, :read, published: 1.day.ago) story2 = create(:story, :read, published: 1.hour.ago) - expect(StoryRepository.read).to eq([story2, story1]) + expect(described_class.read).to eq([story2, story1]) end it "does not return unread stories" do create(:story, :unread) - expect(StoryRepository.read).to be_empty + expect(described_class.read).to be_empty end it "paginates results" do stories = 21.times.map { |num| create(:story, :read, published: num.days.ago) } - expect(StoryRepository.read).to eq(stories[0...20]) - expect(StoryRepository.read(2)).to eq([stories.last]) + expect(described_class.read).to eq(stories[0...20]) + expect(described_class.read(2)).to eq([stories.last]) end end @@ -333,28 +333,28 @@ it "returns starred stories" do story = create(:story, :starred) - expect(StoryRepository.starred).to eq([story]) + expect(described_class.starred).to eq([story]) end it "sorts stories by published" do story1 = create(:story, :starred, published: 1.day.ago) story2 = create(:story, :starred, published: 1.hour.ago) - expect(StoryRepository.starred).to eq([story2, story1]) + expect(described_class.starred).to eq([story2, story1]) end it "does not return unstarred stories" do create(:story) - expect(StoryRepository.starred).to be_empty + expect(described_class.starred).to be_empty end it "paginates results" do stories = 21.times.map { |num| create(:story, :starred, published: num.days.ago) } - expect(StoryRepository.starred).to eq(stories[0...20]) - expect(StoryRepository.starred(2)).to eq([stories.last]) + expect(described_class.starred).to eq(stories[0...20]) + expect(described_class.starred(2)).to eq([stories.last]) end end @@ -362,26 +362,26 @@ it "returns unstarred read stories older than given number of days" do story = create(:story, :read, published: 6.days.ago) - expect(StoryRepository.unstarred_read_stories_older_than(5)) + expect(described_class.unstarred_read_stories_older_than(5)) .to eq([story]) end it "does not return starred stories older than the given number of days" do create(:story, :read, :starred, published: 6.days.ago) - expect(StoryRepository.unstarred_read_stories_older_than(5)).to be_empty + expect(described_class.unstarred_read_stories_older_than(5)).to be_empty end it "does not return unread stories older than the given number of days" do create(:story, :unread, published: 6.days.ago) - expect(StoryRepository.unstarred_read_stories_older_than(5)).to be_empty + expect(described_class.unstarred_read_stories_older_than(5)).to be_empty end it "does not return stories newer than given number of days" do create(:story, :read, published: 4.days.ago) - expect(StoryRepository.unstarred_read_stories_older_than(5)).to be_empty + expect(described_class.unstarred_read_stories_older_than(5)).to be_empty end end @@ -391,13 +391,13 @@ create(:story, :read) create(:story, :read) - expect(StoryRepository.read_count).to eq(3) + expect(described_class.read_count).to eq(3) end it "does not count unread stories" do create_list(:story, 3, :unread) - expect(StoryRepository.read_count).to eq(0) + expect(described_class.read_count).to eq(0) end end @@ -406,21 +406,21 @@ feed = double(url: "http://github.com") entry = double(url: "https://github.com/stringer-rss/stringer") - expect(StoryRepository.extract_url(entry, feed)).to eq "https://github.com/stringer-rss/stringer" + expect(described_class.extract_url(entry, feed)).to eq "https://github.com/stringer-rss/stringer" end it "returns the enclosure_url when the url is nil" do feed = double(url: "http://github.com") entry = double(url: nil, enclosure_url: "https://github.com/stringer-rss/stringer") - expect(StoryRepository.extract_url(entry, feed)).to eq "https://github.com/stringer-rss/stringer" + expect(described_class.extract_url(entry, feed)).to eq "https://github.com/stringer-rss/stringer" end it "does not crash if url is nil but enclosure_url does not exist" do feed = double(url: "http://github.com") entry = double(url: nil) - expect(StoryRepository.extract_url(entry, feed)).to be_nil + expect(described_class.extract_url(entry, feed)).to be_nil end end @@ -428,13 +428,13 @@ it "returns the title if there is a title" do entry = double(title: "title", summary: "summary") - expect(StoryRepository.extract_title(entry)).to eq "title" + expect(described_class.extract_title(entry)).to eq "title" end it "returns the summary if there isn't a title" do entry = double(title: "", summary: "summary") - expect(StoryRepository.extract_title(entry)).to eq "summary" + expect(described_class.extract_title(entry)).to eq "summary" end end @@ -455,18 +455,18 @@ end it "sanitizes content" do - expect(StoryRepository.extract_content(entry)).to eq "Some test content" + expect(described_class.extract_content(entry)).to eq "Some test content" end it "falls back to summary if there is no content" do - expect(StoryRepository.extract_content(summary_only)) + expect(described_class.extract_content(summary_only)) .to eq "Dumb publisher" end it "returns empty string if there is no content or summary" do entry = double(url: "http://mdswanson.com", content: nil, summary: nil) - expect(StoryRepository.extract_content(entry)).to eq "" + expect(described_class.extract_content(entry)).to eq "" end it "expands urls" do @@ -476,7 +476,7 @@ summary: "Page" ) - expect(StoryRepository.extract_content(entry)) + expect(described_class.extract_content(entry)) .to eq "Page" end @@ -484,7 +484,7 @@ entry = double(url: nil, content: nil, summary: "Page") - expect(StoryRepository.extract_content(entry)) + expect(described_class.extract_content(entry)) .to eq "Page" end end diff --git a/spec/repositories/user_repository_spec.rb b/spec/repositories/user_repository_spec.rb index 8e51de51b..b5b56b4fe 100644 --- a/spec/repositories/user_repository_spec.rb +++ b/spec/repositories/user_repository_spec.rb @@ -8,31 +8,31 @@ describe UserRepository do describe ".fetch" do it "returns nil when given id is nil" do - expect(UserRepository.fetch(nil)).to be_nil + expect(described_class.fetch(nil)).to be_nil end it "returns the user for the given id" do user = create(:user) - expect(UserRepository.fetch(user.id)).to eq(user) + expect(described_class.fetch(user.id)).to eq(user) end end describe ".setup_complete?" do it "returns false when there are no users" do - expect(UserRepository.setup_complete?).to be(false) + expect(described_class.setup_complete?).to be(false) end it "returns false when user has not completed setup" do create(:user) - expect(UserRepository.setup_complete?).to be(false) + expect(described_class.setup_complete?).to be(false) end it "returns true when user has completed setup" do create(:user, :setup_complete) - expect(UserRepository.setup_complete?).to be(true) + expect(described_class.setup_complete?).to be(true) end end @@ -40,14 +40,14 @@ it "saves the given user" do user = build(:user) - expect { UserRepository.save(user) } + expect { described_class.save(user) } .to change(user, :persisted?).from(false).to(true) end it "returns the given user" do user = User.new - expect(UserRepository.save(user)).to eq(user) + expect(described_class.save(user)).to eq(user) end end @@ -56,7 +56,7 @@ user = create(:user) create(:user) - expect(UserRepository.first).to eq(user) + expect(described_class.first).to eq(user) end end end diff --git a/spec/tasks/change_password_spec.rb b/spec/tasks/change_password_spec.rb index ef6a5e08b..d93463769 100644 --- a/spec/tasks/change_password_spec.rb +++ b/spec/tasks/change_password_spec.rb @@ -11,7 +11,7 @@ it "invokes command with confirmed password" do output = StringIO.new input = StringIO.new("new-pw\nnew-pw\n") - task = ChangePassword.new(command, output:, input:) + task = described_class.new(command, output:, input:) expect(command).to receive(:change_user_password).with("new-pw") @@ -21,7 +21,7 @@ it "repeats until a matching confirmation" do output = StringIO.new input = StringIO.new("woops\nnope\nnew-pw\nnew-pw\n") - task = ChangePassword.new(command, output:, input:) + task = described_class.new(command, output:, input:) expect(command).to receive(:change_user_password).with("new-pw") diff --git a/spec/tasks/fetch_feed_spec.rb b/spec/tasks/fetch_feed_spec.rb index 532a69166..47e3ec0fc 100644 --- a/spec/tasks/fetch_feed_spec.rb +++ b/spec/tasks/fetch_feed_spec.rb @@ -27,7 +27,12 @@ expect(StoryRepository).not_to receive(:add) - FetchFeed.new(daring_fireball, parser:, client:, logger: nil).fetch + described_class.new( + daring_fireball, + parser:, + client:, + logger: nil + ).fetch end it "logs a message" do @@ -36,7 +41,7 @@ output = StringIO.new logger = Logger.new(output) - FetchFeed.new(daring_fireball, parser:, client:, logger:).fetch + described_class.new(daring_fireball, parser:, client:, logger:).fetch expect(output.string).to include("has not been modified") end @@ -53,7 +58,7 @@ expect(StoryRepository).not_to receive(:add) - FetchFeed.new(daring_fireball, parser:, client:).fetch + described_class.new(daring_fireball, parser:, client:).fetch end end @@ -81,7 +86,7 @@ expect(StoryRepository) .not_to receive(:add).with(old_story, daring_fireball) - FetchFeed.new( + described_class.new( daring_fireball, parser: fake_parser, client: fake_client @@ -92,7 +97,7 @@ expect(FeedRepository).to receive(:update_last_fetched) .with(daring_fireball, now) - FetchFeed.new( + described_class.new( daring_fireball, parser: fake_parser, client: fake_client @@ -109,7 +114,7 @@ expect(FeedRepository).to receive(:set_status) .with(:green, daring_fireball) - FetchFeed.new(daring_fireball, parser:, client:).fetch + described_class.new(daring_fireball, parser:, client:).fetch end it "sets the status to red if things go wrong" do @@ -119,7 +124,12 @@ expect(FeedRepository).to receive(:set_status) .with(:red, daring_fireball) - FetchFeed.new(daring_fireball, parser:, client:, logger: nil).fetch + described_class.new( + daring_fireball, + parser:, + client:, + logger: nil + ).fetch end it "outputs a message when things go wrong" do @@ -128,7 +138,7 @@ output = StringIO.new logger = Logger.new(output) - FetchFeed.new(daring_fireball, parser:, client:, logger:).fetch + described_class.new(daring_fireball, parser:, client:, logger:).fetch expect(output.string).to include("Something went wrong") end diff --git a/spec/tasks/fetch_feeds_spec.rb b/spec/tasks/fetch_feeds_spec.rb index 055b394f6..e16f44723 100644 --- a/spec/tasks/fetch_feeds_spec.rb +++ b/spec/tasks/fetch_feeds_spec.rb @@ -19,7 +19,7 @@ expect(pool).to receive(:shutdown) - FetchFeeds.new(feeds, pool).fetch_all + described_class.new(feeds, pool).fetch_all end it "finds feeds when run after a delay" do @@ -32,7 +32,7 @@ expect(pool).to receive(:shutdown) - FetchFeeds.new(feeds, pool).prepare_to_delay.fetch_all + described_class.new(feeds, pool).prepare_to_delay.fetch_all end end @@ -40,7 +40,7 @@ it "serializes the instance for backgrounding" do feeds = create_pair(:feed) feeds_ids = feeds.map(&:id) - fetch_feeds = FetchFeeds.new(feeds) + fetch_feeds = described_class.new(feeds) fetch_feeds.prepare_to_delay @@ -54,7 +54,8 @@ feeds = create_pair(:feed) feeds_ids = feeds.map(&:id) - expect { FetchFeeds.enqueue(feeds) }.to change(Delayed::Job, :count).by(1) + expect { described_class.enqueue(feeds) } + .to change(Delayed::Job, :count).by(1) job_object = Delayed::Job.last.payload_object.object expect(job_object.instance_variable_get(:@feeds_ids)).to eq(feeds_ids) diff --git a/spec/tasks/remove_old_stories_spec.rb b/spec/tasks/remove_old_stories_spec.rb index 08a36df7f..3ba94dde0 100644 --- a/spec/tasks/remove_old_stories_spec.rb +++ b/spec/tasks/remove_old_stories_spec.rb @@ -12,26 +12,26 @@ end it "passes along the number of days to the story repository query" do - allow(RemoveOldStories).to receive(:pruned_feeds) { [] } + allow(described_class).to receive(:pruned_feeds) { [] } expect(StoryRepository).to receive(:unstarred_read_stories_older_than) .with(7).and_return(stories_mock) - RemoveOldStories.remove!(7) + described_class.remove!(7) end it "requests deletion of all old stories" do - allow(RemoveOldStories).to receive(:pruned_feeds) { [] } + allow(described_class).to receive(:pruned_feeds) { [] } allow(StoryRepository) .to receive(:unstarred_read_stories_older_than) { stories_mock } expect(stories_mock).to receive(:delete_all) - RemoveOldStories.remove!(11) + described_class.remove!(11) end it "fetches affected feeds by id" do - allow(RemoveOldStories).to receive(:old_stories) do + allow(described_class).to receive(:old_stories) do stories = [double("story", feed_id: 3), double("story", feed_id: 5)] allow(stories).to receive(:delete_all) stories @@ -40,20 +40,20 @@ expect(FeedRepository) .to receive(:fetch_by_ids).with([3, 5]).and_return([]) - RemoveOldStories.remove!(13) + described_class.remove!(13) end it "updates last_fetched on affected feeds" do feeds = [double("feed a"), double("feed b")] - allow(RemoveOldStories).to receive(:pruned_feeds) { feeds } - allow(RemoveOldStories).to receive(:old_stories) { stories_mock } + allow(described_class).to receive(:pruned_feeds) { feeds } + allow(described_class).to receive(:old_stories) { stories_mock } expect(FeedRepository) .to receive(:update_last_fetched).with(feeds.first, anything) expect(FeedRepository) .to receive(:update_last_fetched).with(feeds.last, anything) - RemoveOldStories.remove!(13) + described_class.remove!(13) end end end diff --git a/spec/utils/feed_discovery_spec.rb b/spec/utils/feed_discovery_spec.rb index 4266d3ec1..fbf381b04 100644 --- a/spec/utils/feed_discovery_spec.rb +++ b/spec/utils/feed_discovery_spec.rb @@ -20,7 +20,7 @@ expect(parser).to receive(:parse).and_raise(StandardError) expect(finder).to receive(:find).and_return([]) - result = FeedDiscovery.new.discover(url, finder, parser, client) + result = described_class.new.discover(url, finder, parser, client) expect(result).to be(false) end @@ -29,7 +29,7 @@ expect(client).to receive(:get).with(url) expect(parser).to receive(:parse).and_return(feed) - result = FeedDiscovery.new.discover(url, finder, parser, client) + result = described_class.new.discover(url, finder, parser, client) expect(result).to eq feed end @@ -43,7 +43,7 @@ expect(client).to receive(:get).with(invalid_discovered_url) expect(parser).to receive(:parse).and_raise(StandardError) - result = FeedDiscovery.new.discover(url, finder, parser, client) + result = described_class.new.discover(url, finder, parser, client) expect(result).to be(false) end @@ -57,7 +57,7 @@ expect(client).to receive(:get).with(valid_discovered_url) expect(parser).to receive(:parse).and_return(feed) - result = FeedDiscovery.new.discover(url, finder, parser, client) + result = described_class.new.discover(url, finder, parser, client) expect(result).to eq feed end diff --git a/spec/utils/opml_parser_spec.rb b/spec/utils/opml_parser_spec.rb index 0db3f6781..4520a8a07 100644 --- a/spec/utils/opml_parser_spec.rb +++ b/spec/utils/opml_parser_spec.rb @@ -5,7 +5,7 @@ app_require "utils/opml_parser" describe OpmlParser do - let(:parser) { OpmlParser.new } + let(:parser) { described_class.new } describe "#parse_feeds" do it "returns a hash of feed details from an OPML file" do From c7cce410a3511865644027e5c4b5424945f18de4 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 16:07:28 -0800 Subject: [PATCH 0480/1107] RuboCop: enable RSpec/HookArgument cop (#788) --- .rubocop_todo.yml | 8 -------- spec/integration/feed_importing_spec.rb | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index eead9e7dd..d400ec7f9 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -117,14 +117,6 @@ RSpec/FilePath: Exclude: - 'spec/helpers/authentications_helper_spec.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: implicit, each, example -RSpec/HookArgument: - Exclude: - - 'spec/integration/feed_importing_spec.rb' - # Offense count: 6 # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: diff --git a/spec/integration/feed_importing_spec.rb b/spec/integration/feed_importing_spec.rb index 291d93dda..8481e2558 100644 --- a/spec/integration/feed_importing_spec.rb +++ b/spec/integration/feed_importing_spec.rb @@ -34,7 +34,7 @@ end describe "Importing for the second time" do - before(:each) do + before do @server.response = sample_data("feeds/feed01_valid_feed/feed.xml") fetch_feed(feed) end From 8e3180a3b08a874cafe6d5606c04c0ee6851d690 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 16:11:51 -0800 Subject: [PATCH 0481/1107] RuboCop: enable RSpec/InstanceVariable cop (#789) --- .rubocop_todo.yml | 6 ------ spec/integration/feed_importing_spec.rb | 15 +++++++-------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d400ec7f9..0badbc720 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -117,12 +117,6 @@ RSpec/FilePath: Exclude: - 'spec/helpers/authentications_helper_spec.rb' -# Offense count: 6 -# Configuration parameters: AssignmentOnly. -RSpec/InstanceVariable: - Exclude: - - 'spec/integration/feed_importing_spec.rb' - # Offense count: 8 # This cop supports safe autocorrection (--autocorrect). RSpec/LeadingSubject: diff --git a/spec/integration/feed_importing_spec.rb b/spec/integration/feed_importing_spec.rb index 8481e2558..90c0f3771 100644 --- a/spec/integration/feed_importing_spec.rb +++ b/spec/integration/feed_importing_spec.rb @@ -9,13 +9,12 @@ app_require "tasks/fetch_feed" describe "Feed importing" do - before { @server = FeedServer.new } - + let(:server) { FeedServer.new } let(:feed) do Feed.create( name: "Example feed", last_fetched: Time.new(2014, 1, 1), - url: @server.url + url: server.url ) end @@ -28,27 +27,27 @@ describe "Importing for the first time" do it "imports all entries" do - @server.response = sample_data("feeds/feed01_valid_feed/feed.xml") + server.response = sample_data("feeds/feed01_valid_feed/feed.xml") expect { fetch_feed(feed) }.to change(feed.stories, :count).to(5) end end describe "Importing for the second time" do before do - @server.response = sample_data("feeds/feed01_valid_feed/feed.xml") + server.response = sample_data("feeds/feed01_valid_feed/feed.xml") fetch_feed(feed) end context "no new entries" do it "does not create new stories" do - @server.response = sample_data("feeds/feed01_valid_feed/feed.xml") + server.response = sample_data("feeds/feed01_valid_feed/feed.xml") expect { fetch_feed(feed) }.to_not change(feed.stories, :count) end end context "new entries" do it "creates new stories" do - @server.response = + server.response = sample_data("feeds/feed01_valid_feed/feed_updated.xml") expect { fetch_feed(feed) }.to change(feed.stories, :count).by(1) end @@ -71,7 +70,7 @@ # was published. feed.last_fetched = Time.parse("2014-08-12T00:01:00Z") - @server.response = + server.response = sample_data("feeds/feed02_invalid_published_dates/feed.xml") expect { fetch_feed(feed) }.to change { feed.stories.count }.by(1) From 55e91bf3933717150b302f4b6b0ec42def665134 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 16:24:47 -0800 Subject: [PATCH 0482/1107] RuboCop: enable RSpec/LeadingSubject cop (#790) --- .rubocop_todo.yml | 13 ------------- spec/fever_api/read_feeds_groups_spec.rb | 4 ++-- spec/fever_api/read_feeds_spec.rb | 4 ++-- spec/fever_api/read_groups_spec.rb | 4 ++-- spec/fever_api/read_items_spec.rb | 4 ++-- spec/fever_api/sync_saved_item_ids_spec.rb | 4 ++-- spec/fever_api/sync_unread_item_ids_spec.rb | 4 ++-- spec/fever_api/write_mark_feed_spec.rb | 4 ++-- spec/fever_api/write_mark_group_spec.rb | 4 ++-- 9 files changed, 16 insertions(+), 29 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 0badbc720..59af1bc53 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -117,19 +117,6 @@ RSpec/FilePath: Exclude: - 'spec/helpers/authentications_helper_spec.rb' -# Offense count: 8 -# This cop supports safe autocorrection (--autocorrect). -RSpec/LeadingSubject: - Exclude: - - 'spec/fever_api/read_feeds_groups_spec.rb' - - 'spec/fever_api/read_feeds_spec.rb' - - 'spec/fever_api/read_groups_spec.rb' - - 'spec/fever_api/read_items_spec.rb' - - 'spec/fever_api/sync_saved_item_ids_spec.rb' - - 'spec/fever_api/sync_unread_item_ids_spec.rb' - - 'spec/fever_api/write_mark_feed_spec.rb' - - 'spec/fever_api/write_mark_group_spec.rb' - # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). RSpec/LetBeforeExamples: diff --git a/spec/fever_api/read_feeds_groups_spec.rb b/spec/fever_api/read_feeds_groups_spec.rb index ce98c876f..21b80bc8f 100644 --- a/spec/fever_api/read_feeds_groups_spec.rb +++ b/spec/fever_api/read_feeds_groups_spec.rb @@ -5,12 +5,12 @@ app_require "fever_api/read_feeds_groups" describe FeverAPI::ReadFeedsGroups do + subject { described_class.new(feed_repository:) } + let(:feed_ids) { [5, 7, 11] } let(:feeds) { feed_ids.map { |id| double("feed", id:, group_id: 1) } } let(:feed_repository) { double("repo") } - subject { described_class.new(feed_repository:) } - it "returns a list of groups requested through feeds" do allow(feed_repository) .to receive_message_chain(:in_group, :order).and_return(feeds) diff --git a/spec/fever_api/read_feeds_spec.rb b/spec/fever_api/read_feeds_spec.rb index a49766d98..3ab78665a 100644 --- a/spec/fever_api/read_feeds_spec.rb +++ b/spec/fever_api/read_feeds_spec.rb @@ -5,14 +5,14 @@ app_require "fever_api/read_feeds" describe FeverAPI::ReadFeeds do + subject { described_class.new(feed_repository:) } + let(:feed_ids) { [5, 7, 11] } let(:feeds) do feed_ids.map { |id| double("feed", id:, as_fever_json: { id: }) } end let(:feed_repository) { double("repo") } - subject { described_class.new(feed_repository:) } - it "returns a list of feeds" do expect(feed_repository).to receive(:list).and_return(feeds) expect(subject.call("feeds" => nil)).to eq( diff --git a/spec/fever_api/read_groups_spec.rb b/spec/fever_api/read_groups_spec.rb index 176429b41..198ecb881 100644 --- a/spec/fever_api/read_groups_spec.rb +++ b/spec/fever_api/read_groups_spec.rb @@ -5,14 +5,14 @@ app_require "fever_api/read_groups" describe FeverAPI::ReadGroups do + subject { described_class.new(group_repository:) } + let(:group1) { double("group1", as_fever_json: { id: 1, title: "IT news" }) } let(:group2) do double("group2", as_fever_json: { id: 2, title: "World news" }) end let(:group_repository) { double("repo") } - subject { described_class.new(group_repository:) } - it "returns a group list if requested" do expect(group_repository).to receive(:list).and_return([group1, group2]) expect(subject.call("groups" => nil)).to eq( diff --git a/spec/fever_api/read_items_spec.rb b/spec/fever_api/read_items_spec.rb index cd71d5511..89ad94741 100644 --- a/spec/fever_api/read_items_spec.rb +++ b/spec/fever_api/read_items_spec.rb @@ -5,10 +5,10 @@ app_require "fever_api/read_items" describe FeverAPI::ReadItems do - let(:story_repository) { double("repo") } - subject { described_class.new(story_repository:) } + let(:story_repository) { double("repo") } + it "returns a list of unread items including total count" do stories = [ double("story", as_fever_json: { id: 5 }), diff --git a/spec/fever_api/sync_saved_item_ids_spec.rb b/spec/fever_api/sync_saved_item_ids_spec.rb index daef8ce43..fad1b5b3c 100644 --- a/spec/fever_api/sync_saved_item_ids_spec.rb +++ b/spec/fever_api/sync_saved_item_ids_spec.rb @@ -5,12 +5,12 @@ app_require "fever_api/sync_saved_item_ids" describe FeverAPI::SyncSavedItemIds do + subject { described_class.new(story_repository:) } + let(:story_ids) { [5, 7, 11] } let(:stories) { story_ids.map { |id| double("story", id:) } } let(:story_repository) { double("repo") } - subject { described_class.new(story_repository:) } - it "returns a list of starred items if requested" do expect(story_repository).to receive(:all_starred).and_return(stories) expect(subject.call("saved_item_ids" => nil)) diff --git a/spec/fever_api/sync_unread_item_ids_spec.rb b/spec/fever_api/sync_unread_item_ids_spec.rb index f46b7ff39..bbdea8c6a 100644 --- a/spec/fever_api/sync_unread_item_ids_spec.rb +++ b/spec/fever_api/sync_unread_item_ids_spec.rb @@ -5,12 +5,12 @@ app_require "fever_api/sync_unread_item_ids" describe FeverAPI::SyncUnreadItemIds do + subject { described_class.new(story_repository:) } + let(:story_ids) { [5, 7, 11] } let(:stories) { story_ids.map { |id| double("story", id:) } } let(:story_repository) { double("repo") } - subject { described_class.new(story_repository:) } - it "returns a list of unread items if requested" do expect(story_repository).to receive(:unread).and_return(stories) expect(subject.call("unread_item_ids" => nil)) diff --git a/spec/fever_api/write_mark_feed_spec.rb b/spec/fever_api/write_mark_feed_spec.rb index ee7c13842..429b37512 100644 --- a/spec/fever_api/write_mark_feed_spec.rb +++ b/spec/fever_api/write_mark_feed_spec.rb @@ -5,11 +5,11 @@ app_require "fever_api/write_mark_feed" describe FeverAPI::WriteMarkFeed do + subject { described_class.new(marker_class:) } + let(:feed_marker) { double("feed marker") } let(:marker_class) { double("marker class") } - subject { described_class.new(marker_class:) } - it "instantiates a feed marker and calls mark_feed_as_read if requested" do expect(marker_class) .to receive(:new).with(5, 1234567890).and_return(feed_marker) diff --git a/spec/fever_api/write_mark_group_spec.rb b/spec/fever_api/write_mark_group_spec.rb index 16a955cfa..b0f209704 100644 --- a/spec/fever_api/write_mark_group_spec.rb +++ b/spec/fever_api/write_mark_group_spec.rb @@ -5,11 +5,11 @@ app_require "fever_api/write_mark_group" describe FeverAPI::WriteMarkGroup do + subject { described_class.new(marker_class:) } + let(:group_marker) { double("group marker") } let(:marker_class) { double("marker class") } - subject { described_class.new(marker_class:) } - it "instantiates a group marker and calls mark_group_as_read if requested" do expect(marker_class) .to receive(:new).with(5, 1234567890).and_return(group_marker) From c9b76bbe3eb31197d8c2559317f5da21fb9cfa90 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 16:30:31 -0800 Subject: [PATCH 0483/1107] RuboCop: enable RSpec/LetBeforeExamples cop (#791) --- .rubocop_todo.yml | 6 ------ spec/repositories/feed_repository_spec.rb | 3 +-- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 59af1bc53..9ba0bce1c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -117,12 +117,6 @@ RSpec/FilePath: Exclude: - 'spec/helpers/authentications_helper_spec.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -RSpec/LetBeforeExamples: - Exclude: - - 'spec/repositories/feed_repository_spec.rb' - # Offense count: 4 RSpec/MessageChain: Exclude: diff --git a/spec/repositories/feed_repository_spec.rb b/spec/repositories/feed_repository_spec.rb index f9da37222..fbcbaef5d 100644 --- a/spec/repositories/feed_repository_spec.rb +++ b/spec/repositories/feed_repository_spec.rb @@ -60,9 +60,8 @@ expect(feed.last_fetched).to eq timestamp end - let(:weird_timestamp) { Time.parse("Mon, 01 Jan 0001 00:00:00 +0100") } - it "rejects weird timestamps" do + weird_timestamp = Time.parse("Mon, 01 Jan 0001 00:00:00 +0100") feed = Feed.new(last_fetched: timestamp) described_class.update_last_fetched(feed, weird_timestamp) From 02ea93052c26a59f84f19579d8ec20dd7adb2678 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 16:34:54 -0800 Subject: [PATCH 0484/1107] RuboCop: enable RSpec/NoExpectationExample cop (#792) --- .rubocop_todo.yml | 7 ------- spec/commands/stories/mark_group_as_read_spec.rb | 7 ++++--- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 9ba0bce1c..f5d137377 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -211,13 +211,6 @@ RSpec/NestedGroups: - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/integration/feed_importing_spec.rb' -# Offense count: 2 -# Configuration parameters: AllowedPatterns. -# AllowedPatterns: ^expect_, ^assert_ -RSpec/NoExpectationExample: - Exclude: - - 'spec/commands/stories/mark_group_as_read_spec.rb' - # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. diff --git a/spec/commands/stories/mark_group_as_read_spec.rb b/spec/commands/stories/mark_group_as_read_spec.rb index 570e19127..1f8f51d13 100644 --- a/spec/commands/stories/mark_group_as_read_spec.rb +++ b/spec/commands/stories/mark_group_as_read_spec.rb @@ -31,17 +31,18 @@ def run_command(group_id) end context "SPARKS_GROUP_ID and KINDLING_GROUP_ID" do - before do + it "marks as read all feeds when group is 0" do expect(stories).to receive(:update_all).with(is_read: true) expect(repo).to receive(:fetch_unread_by_timestamp).and_return(stories) - end - it "marks as read all feeds when group is 0" do command = run_command(0) command.mark_group_as_read end it "marks as read all feeds when group is -1" do + expect(stories).to receive(:update_all).with(is_read: true) + expect(repo).to receive(:fetch_unread_by_timestamp).and_return(stories) + command = run_command(-1) command.mark_group_as_read end From 85345368f3aa5d915bd6ef05b957a8fe8c190a4a Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 16:39:00 -0800 Subject: [PATCH 0485/1107] RuboCop: enable RSpec/NotToNot cop (#793) --- .rubocop_todo.yml | 8 -------- spec/integration/feed_importing_spec.rb | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f5d137377..f7df262a4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -211,14 +211,6 @@ RSpec/NestedGroups: - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/integration/feed_importing_spec.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: not_to, to_not -RSpec/NotToNot: - Exclude: - - 'spec/integration/feed_importing_spec.rb' - # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. diff --git a/spec/integration/feed_importing_spec.rb b/spec/integration/feed_importing_spec.rb index 90c0f3771..d6227295b 100644 --- a/spec/integration/feed_importing_spec.rb +++ b/spec/integration/feed_importing_spec.rb @@ -41,7 +41,7 @@ context "no new entries" do it "does not create new stories" do server.response = sample_data("feeds/feed01_valid_feed/feed.xml") - expect { fetch_feed(feed) }.to_not change(feed.stories, :count) + expect { fetch_feed(feed) }.not_to change(feed.stories, :count) end end From f62f0bcb8c2b4521693e1e05acdf54631b6781b4 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 16:43:27 -0800 Subject: [PATCH 0486/1107] RuboCop: enable RSpec/ReturnFromStub cop (#794) --- .rubocop_todo.yml | 8 -------- spec/tasks/remove_old_stories_spec.rb | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f7df262a4..a16d1ab06 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -211,14 +211,6 @@ RSpec/NestedGroups: - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/integration/feed_importing_spec.rb' -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: and_return, block -RSpec/ReturnFromStub: - Exclude: - - 'spec/tasks/remove_old_stories_spec.rb' - # Offense count: 3 # This cop supports safe autocorrection (--autocorrect). RSpec/ScatteredLet: diff --git a/spec/tasks/remove_old_stories_spec.rb b/spec/tasks/remove_old_stories_spec.rb index 3ba94dde0..960eedbf6 100644 --- a/spec/tasks/remove_old_stories_spec.rb +++ b/spec/tasks/remove_old_stories_spec.rb @@ -12,7 +12,7 @@ end it "passes along the number of days to the story repository query" do - allow(described_class).to receive(:pruned_feeds) { [] } + allow(described_class).to receive(:pruned_feeds).and_return([]) expect(StoryRepository).to receive(:unstarred_read_stories_older_than) .with(7).and_return(stories_mock) @@ -21,7 +21,7 @@ end it "requests deletion of all old stories" do - allow(described_class).to receive(:pruned_feeds) { [] } + allow(described_class).to receive(:pruned_feeds).and_return([]) allow(StoryRepository) .to receive(:unstarred_read_stories_older_than) { stories_mock } From 6d8d26feb1c84f409592e73bb2a0115c700ba429 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 16:47:54 -0800 Subject: [PATCH 0487/1107] RuboCop: enable RSpec/ScatteredLet cop (#795) --- .rubocop_todo.yml | 7 ------- spec/commands/feeds/import_from_opml_spec.rb | 5 ++--- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index a16d1ab06..e763e1f3f 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -211,13 +211,6 @@ RSpec/NestedGroups: - 'spec/commands/feeds/add_new_feed_spec.rb' - 'spec/integration/feed_importing_spec.rb' -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -RSpec/ScatteredLet: - Exclude: - - 'spec/commands/feeds/import_from_opml_spec.rb' - - 'spec/repositories/feed_repository_spec.rb' - # Offense count: 94 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: diff --git a/spec/commands/feeds/import_from_opml_spec.rb b/spec/commands/feeds/import_from_opml_spec.rb index 0b4a08643..9e668ae30 100644 --- a/spec/commands/feeds/import_from_opml_spec.rb +++ b/spec/commands/feeds/import_from_opml_spec.rb @@ -13,6 +13,8 @@ ) ) end + let(:group1) { Group.find_by_name("Football News") } + let(:group2) { Group.find_by_name("RoR") } def import described_class.import(subscriptions) @@ -23,9 +25,6 @@ def import Group.delete_all end - let(:group1) { Group.find_by_name("Football News") } - let(:group2) { Group.find_by_name("RoR") } - context "adding group_id for existing feeds" do let!(:feed1) do Feed.create(name: "TMW Football Transfer News", url: "http://www.transfermarketweb.com/rss") From 2af578ebba2fb01ecb53389b567a2b773fa58015 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 16:52:09 -0800 Subject: [PATCH 0488/1107] RuboCop: disable Rails/BulkChangeTable cop (#796) --- .rubocop.yml | 1 + .rubocop_todo.yml | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index de898a51e..eb9f20122 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -40,6 +40,7 @@ Bundler/GemComment: { Enabled: false } Bundler/GemVersion: { Enabled: false } Layout/SingleLineBlockChain: { Enabled: false } Lint/ConstantResolution: { Enabled: false } +Rails/BulkChangeTable: { Enabled: false } RSpec/AlignLeftLetBrace: { Enabled: false } RSpec/AlignRightLetBrace: { Enabled: false } RSpec/StubbedMock: { Enabled: false } diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e763e1f3f..c7fc51433 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -237,15 +237,6 @@ RSpec/VerifiedDoubles: - 'spec/tasks/remove_old_stories_spec.rb' - 'spec/utils/feed_discovery_spec.rb' -# Offense count: 2 -# Configuration parameters: Database, Include. -# SupportedDatabases: mysql, postgresql -# Include: db/migrate/*.rb -Rails/BulkChangeTable: - Exclude: - - 'db/migrate/20130412185253_add_new_fields_to_stories.rb' - - 'db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb' - # Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Whitelist, AllowedMethods, AllowedReceivers. From c9fbda3728e51b67870ed697a055220be7d1c06e Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 16:59:33 -0800 Subject: [PATCH 0489/1107] RuboCop: enable Rails/DynamicFindBy cop (#797) --- .rubocop_todo.yml | 10 ---------- spec/commands/feeds/import_from_opml_spec.rb | 6 +++--- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c7fc51433..700657864 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -237,16 +237,6 @@ RSpec/VerifiedDoubles: - 'spec/tasks/remove_old_stories_spec.rb' - 'spec/utils/feed_discovery_spec.rb' -# Offense count: 3 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Whitelist, AllowedMethods, AllowedReceivers. -# Whitelist: find_by_sql, find_by_token_for -# AllowedMethods: find_by_sql, find_by_token_for -# AllowedReceivers: Gem::Specification, page -Rails/DynamicFindBy: - Exclude: - - 'spec/commands/feeds/import_from_opml_spec.rb' - # Offense count: 1 # Configuration parameters: Include. # Include: app/models/**/*.rb diff --git a/spec/commands/feeds/import_from_opml_spec.rb b/spec/commands/feeds/import_from_opml_spec.rb index 9e668ae30..2c6642826 100644 --- a/spec/commands/feeds/import_from_opml_spec.rb +++ b/spec/commands/feeds/import_from_opml_spec.rb @@ -13,8 +13,8 @@ ) ) end - let(:group1) { Group.find_by_name("Football News") } - let(:group2) { Group.find_by_name("RoR") } + let(:group1) { Group.find_by(name: "Football News") } + let(:group2) { Group.find_by(name: "RoR") } def import described_class.import(subscriptions) @@ -93,7 +93,7 @@ def import it "does not create empty group" do described_class.import(subscriptions) - expect(Group.find_by_name("Empty Group")).to be_nil + expect(Group.find_by(name: "Empty Group")).to be_nil end end From 25100c451e62c8df5889fbfa0350f136b4c180c0 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 17:03:46 -0800 Subject: [PATCH 0490/1107] RuboCop: enable a couple of Rails/Where cops (#798) --- .rubocop_todo.yml | 13 ------------- app/models/feed.rb | 2 +- app/repositories/story_repository.rb | 2 +- spec/commands/feeds/import_from_opml_spec.rb | 2 +- 4 files changed, 3 insertions(+), 16 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 700657864..12e098106 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -347,19 +347,6 @@ Rails/Validation: - 'app/models/feed.rb' - 'app/models/story.rb' -# Offense count: 2 -# This cop supports unsafe autocorrection (--autocorrect-all). -Rails/WhereEquals: - Exclude: - - 'app/models/feed.rb' - - 'app/repositories/story_repository.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Rails/WhereNot: - Exclude: - - 'spec/commands/feeds/import_from_opml_spec.rb' - # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedVars. diff --git a/app/models/feed.rb b/app/models/feed.rb index 266bd32e2..29ae94305 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -17,7 +17,7 @@ def status_bubble end def unread_stories - stories.where("is_read = ?", false) + stories.where(is_read: false) end def as_fever_json diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index 1380b7045..bee6f822d 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -61,7 +61,7 @@ def self.unread_since_id(since_id) end def self.feed(feed_id) - Story.where("feed_id = ?", feed_id).order("published desc").includes(:feed) + Story.where(feed_id:).order("published desc").includes(:feed) end def self.read(page = 1) diff --git a/spec/commands/feeds/import_from_opml_spec.rb b/spec/commands/feeds/import_from_opml_spec.rb index 2c6642826..16d22d658 100644 --- a/spec/commands/feeds/import_from_opml_spec.rb +++ b/spec/commands/feeds/import_from_opml_spec.rb @@ -104,7 +104,7 @@ def import it "does not create any new group for feeds without group" do described_class.import(subscriptions) - expect(Group.where("id NOT IN (?)", [group1.id, group2.id]).count).to eq 0 + expect(Group.where.not(id: [group1.id, group2.id]).count).to eq 0 end it "creates feeds without group_id" do From 24f9cacd911b00d91ba00ff237d9e5eb0770b5af Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Tue, 3 Jan 2023 17:03:53 -0800 Subject: [PATCH 0491/1107] Update all Bundler dependencies (2023-01-04) (#772) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- Gemfile.lock | 16 ++++++++-------- app/commands/stories/mark_all_as_read.rb | 8 ++++---- app/commands/users/create_user.rb | 8 ++++---- app/tasks/fetch_feeds.rb | 8 ++++---- app/utils/sample_story.rb | 2 +- .../20130821020313_update_nil_entry_ids.rb | 2 +- ...2_use_text_datatype_for_title_and_entry_id.rb | 2 +- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4f0243833..9a1cbe79e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -136,7 +136,7 @@ GEM method_source (1.0.0) mini_mime (1.1.2) mini_portile2 (2.8.1) - minitest (5.16.3) + minitest (5.17.0) multi_json (1.15.0) multi_xml (0.6.0) mustermann (3.0.0) @@ -155,7 +155,7 @@ GEM mini_portile2 (~> 2.8.0) racc (~> 1.4) parallel (1.22.1) - parser (3.1.3.0) + parser (3.2.0.0) ast (~> 2.4.1) pg (1.4.5) pry (0.14.1) @@ -165,10 +165,10 @@ GEM byebug (~> 11.0) pry (>= 0.13, < 0.15) public_suffix (5.0.1) - puma (6.0.1) + puma (6.0.2) nio4r (~> 2.0) racc (1.6.2) - rack (2.2.4) + rack (2.2.5) rack-protection (3.0.5) rack rack-ssl (1.4.1) @@ -232,17 +232,17 @@ GEM rspec-mocks (~> 3.11) rspec-support (~> 3.11) rspec-support (3.12.0) - rubocop (1.41.1) + rubocop (1.42.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.1.2.1) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.23.0, < 2.0) + rubocop-ast (>= 1.24.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.24.0) + rubocop-ast (1.24.1) parser (>= 3.1.1.0) rubocop-rails (2.17.4) activesupport (>= 4.2.0) @@ -303,7 +303,7 @@ GEM concurrent-ruby (~> 1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) - unicode-display_width (2.3.0) + unicode-display_width (2.4.1) webmock (3.18.1) addressable (>= 2.8.0) crack (>= 0.3.2) diff --git a/app/commands/stories/mark_all_as_read.rb b/app/commands/stories/mark_all_as_read.rb index c13d3aa76..62d060a05 100644 --- a/app/commands/stories/mark_all_as_read.rb +++ b/app/commands/stories/mark_all_as_read.rb @@ -3,15 +3,15 @@ require_relative "../../repositories/story_repository" class MarkAllAsRead + def self.call(*args) + new(*args).call + end + def initialize(story_ids, repository = StoryRepository) @story_ids = story_ids @repo = repository end - def self.call(*args) - new(*args).call - end - def call @repo.fetch_by_ids(@story_ids).update_all(is_read: true) end diff --git a/app/commands/users/create_user.rb b/app/commands/users/create_user.rb index cb1765ff7..b183135ef 100644 --- a/app/commands/users/create_user.rb +++ b/app/commands/users/create_user.rb @@ -3,14 +3,14 @@ require_relative "../../utils/api_key" class CreateUser - def initialize(repository = User) - @repo = repository - end - def self.call(password) new.call(password) end + def initialize(repository = User) + @repo = repository + end + def call(password) @repo.delete_all @repo.create( diff --git a/app/tasks/fetch_feeds.rb b/app/tasks/fetch_feeds.rb index 8a45c01c9..b7d351551 100644 --- a/app/tasks/fetch_feeds.rb +++ b/app/tasks/fetch_feeds.rb @@ -7,6 +7,10 @@ require "delayed_job_active_record" class FetchFeeds + def self.enqueue(feeds) + new(feeds).prepare_to_delay.delay.fetch_all + end + def initialize(feeds, pool = nil) @pool = pool @feeds = feeds @@ -36,8 +40,4 @@ def prepare_to_delay @feeds = [] self end - - def self.enqueue(feeds) - new(feeds).prepare_to_delay.delay.fetch_all - end end diff --git a/app/utils/sample_story.rb b/app/utils/sample_story.rb index 16c0bd1b4..6db079814 100644 --- a/app/utils/sample_story.rb +++ b/app/utils/sample_story.rb @@ -24,7 +24,7 @@ SampleStory = Struct.new(:source, :title, :lead, :is_read, :published) do def id - -1 * rand(100) + rand(100) * -1 end def headline diff --git a/db/migrate/20130821020313_update_nil_entry_ids.rb b/db/migrate/20130821020313_update_nil_entry_ids.rb index 1f3a90416..f0f7729b5 100644 --- a/db/migrate/20130821020313_update_nil_entry_ids.rb +++ b/db/migrate/20130821020313_update_nil_entry_ids.rb @@ -8,7 +8,7 @@ def up end end - def self.down + def down # skip end end diff --git a/db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb b/db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb index b2ad28328..1d065d2b1 100644 --- a/db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb +++ b/db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb @@ -6,7 +6,7 @@ def up change_column :stories, :entry_id, :text end - def self.down + def down change_column :stories, :title, :string change_column :stories, :entry_id, :string end From 4518995ed98c82776968fd444878f0d2f08ebdd3 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 17:13:11 -0800 Subject: [PATCH 0492/1107] RuboCop: add parens for Rakefile (#799) --- .rubocop.yml | 2 ++ .rubocop_todo.yml | 1 - Rakefile | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index eb9f20122..5e7e26fad 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -22,6 +22,8 @@ Style/MethodCallWithArgsParentheses: - to - not_to - describe + - require + - task Style/StringLiterals: { EnforcedStyle: double_quotes } Style/SymbolArray: { EnforcedStyle: brackets } Style/WordArray: { EnforcedStyle: brackets } diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 12e098106..adcde89f8 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -360,7 +360,6 @@ Style/FetchEnvVar: # SupportedStyles: require_parentheses, omit_parentheses Style/MethodCallWithArgsParentheses: Exclude: - - 'Rakefile' - 'app.rb' - 'app/commands/feeds/export_to_opml.rb' - 'app/models/feed.rb' diff --git a/Rakefile b/Rakefile index 87fc524b4..c15fe9554 100644 --- a/Rakefile +++ b/Rakefile @@ -37,7 +37,7 @@ task :lazy_fetch do end FeedRepository.list.each do |feed| - Delayed::Job.enqueue FetchFeedJob.new(feed.id) + Delayed::Job.enqueue(FetchFeedJob.new(feed.id)) end end From 67370f7c2477e16064dc0c60bf2e6fc3353cc8d7 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 17:17:52 -0800 Subject: [PATCH 0493/1107] RuboCop: add parens for app files (#800) --- .rubocop_todo.yml | 5 ----- app.rb | 6 +++--- app/commands/feeds/export_to_opml.rb | 2 +- app/models/feed.rb | 2 +- app/tasks/change_password.rb | 2 +- app/tasks/fetch_feed.rb | 4 ++-- 6 files changed, 8 insertions(+), 13 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index adcde89f8..ab8b2a03e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -360,11 +360,6 @@ Style/FetchEnvVar: # SupportedStyles: require_parentheses, omit_parentheses Style/MethodCallWithArgsParentheses: Exclude: - - 'app.rb' - - 'app/commands/feeds/export_to_opml.rb' - - 'app/models/feed.rb' - - 'app/tasks/change_password.rb' - - 'app/tasks/fetch_feed.rb' - 'config/asset_pipeline.rb' - 'config/puma.rb' - 'db/migrate/20130409010818_create_feeds.rb' diff --git a/app.rb b/app.rb index 24225afb0..d74d6df97 100644 --- a/app.rb +++ b/app.rb @@ -69,15 +69,15 @@ class Stringer < Sinatra::Base include Sinatra::AuthenticationHelpers def render_partial(name, locals = {}) - erb "partials/_#{name}".to_sym, layout: false, locals: + erb("partials/_#{name}".to_sym, layout: false, locals:) end def render_js_template(name) - erb "js/templates/_#{name}.js".to_sym, layout: false + erb("js/templates/_#{name}.js".to_sym, layout: false) end def render_js(name, locals = {}) - erb "js/#{name}.js".to_sym, layout: false, locals: + erb("js/#{name}.js".to_sym, layout: false, locals:) end def t(*args, **kwargs) diff --git a/app/commands/feeds/export_to_opml.rb b/app/commands/feeds/export_to_opml.rb index b6a859089..0ca5754eb 100644 --- a/app/commands/feeds/export_to_opml.rb +++ b/app/commands/feeds/export_to_opml.rb @@ -11,7 +11,7 @@ def to_xml builder = Nokogiri::XML::Builder.new do |xml| xml.opml(version: "1.0") do - xml.head { xml.title "Feeds from Stringer" } + xml.head { xml.title("Feeds from Stringer") } xml.body do @feeds.each do |feed| xml.outline( diff --git a/app/models/feed.rb b/app/models/feed.rb index 29ae94305..b214aba50 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -3,7 +3,7 @@ require_relative "./application_record" class Feed < ApplicationRecord - has_many :stories, -> { order "published desc" }, dependent: :delete_all + has_many :stories, -> { order("published desc") }, dependent: :delete_all belongs_to :group validates_uniqueness_of :url diff --git a/app/tasks/change_password.rb b/app/tasks/change_password.rb index b59e6fb30..c8445566d 100644 --- a/app/tasks/change_password.rb +++ b/app/tasks/change_password.rb @@ -17,7 +17,7 @@ def initialize( def change_password while (password = ask_password) != ask_confirmation - @output.puts I18n.t("first_run.flash.passwords_dont_match") + @output.puts(I18n.t("first_run.flash.passwords_dont_match")) end @command.change_user_password(password) end diff --git a/app/tasks/fetch_feed.rb b/app/tasks/fetch_feed.rb index 5278f1423..4d839884e 100644 --- a/app/tasks/fetch_feed.rb +++ b/app/tasks/fetch_feed.rb @@ -33,7 +33,7 @@ def fetch rescue StandardError => e FeedRepository.set_status(:red, @feed) - @logger&.error "Something went wrong when parsing #{@feed.url}: #{e}" + @logger&.error("Something went wrong when parsing #{@feed.url}: #{e}") end private @@ -44,7 +44,7 @@ def fetch_raw_feed end def feed_not_modified - @logger&.info "#{@feed.url} has not been modified since last fetch" + @logger&.info("#{@feed.url} has not been modified since last fetch") end def feed_modified(raw_feed) From e6dc80f0ab52c7daad8373d5c0fb7bad6a9600bd Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 17:24:00 -0800 Subject: [PATCH 0494/1107] RuboCop: add parens for command specs (#801) --- .rubocop_todo.yml | 6 ------ spec/commands/feeds/add_new_feed_spec.rb | 2 +- spec/commands/feeds/export_to_opml_spec.rb | 10 +++++----- spec/commands/feeds/import_from_opml_spec.rb | 10 +++++----- spec/commands/find_new_stories_spec.rb | 4 ++-- spec/commands/users/change_user_password_spec.rb | 4 ++-- spec/commands/users/sign_in_user_spec.rb | 2 +- 7 files changed, 16 insertions(+), 22 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index ab8b2a03e..58db54f8f 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -381,12 +381,6 @@ Style/MethodCallWithArgsParentheses: - 'db/migrate/20130805113712_update_stories_unique_constraints.rb' - 'db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb' - 'db/migrate/20140413100725_add_groups_table_and_foreign_keys_to_feeds.rb' - - 'spec/commands/feeds/add_new_feed_spec.rb' - - 'spec/commands/feeds/export_to_opml_spec.rb' - - 'spec/commands/feeds/import_from_opml_spec.rb' - - 'spec/commands/find_new_stories_spec.rb' - - 'spec/commands/users/change_user_password_spec.rb' - - 'spec/commands/users/sign_in_user_spec.rb' - 'spec/fever_api_spec.rb' - 'spec/helpers/url_helpers_spec.rb' - 'spec/integration/feed_importing_spec.rb' diff --git a/spec/commands/feeds/add_new_feed_spec.rb b/spec/commands/feeds/add_new_feed_spec.rb index dc2df8187..93a17e731 100644 --- a/spec/commands/feeds/add_new_feed_spec.rb +++ b/spec/commands/feeds/add_new_feed_spec.rb @@ -28,7 +28,7 @@ result = described_class.add("http://feed.com", discoverer, repo) - expect(result).to be feed + expect(result).to be(feed) end context "title includes a script tag" do diff --git a/spec/commands/feeds/export_to_opml_spec.rb b/spec/commands/feeds/export_to_opml_spec.rb index 01c89a82e..9d136f19a 100644 --- a/spec/commands/feeds/export_to_opml_spec.rb +++ b/spec/commands/feeds/export_to_opml_spec.rb @@ -15,10 +15,10 @@ outlines = Nokogiri.XML(result).xpath("//body//outline") expect(outlines.size).to eq(2) - expect(outlines.first["title"]).to eq feed_one.name - expect(outlines.first["xmlUrl"]).to eq feed_one.url - expect(outlines.last["title"]).to eq feed_two.name - expect(outlines.last["xmlUrl"]).to eq feed_two.url + expect(outlines.first["title"]).to eq(feed_one.name) + expect(outlines.first["xmlUrl"]).to eq(feed_one.url) + expect(outlines.last["title"]).to eq(feed_two.name) + expect(outlines.last["xmlUrl"]).to eq(feed_two.url) end it "handles empty feeds" do @@ -32,7 +32,7 @@ result = described_class.new(feeds).to_xml title = Nokogiri.XML(result).xpath("//head//title").first - expect(title.content).to eq "Feeds from Stringer" + expect(title.content).to eq("Feeds from Stringer") end end end diff --git a/spec/commands/feeds/import_from_opml_spec.rb b/spec/commands/feeds/import_from_opml_spec.rb index 16d22d658..00bb8722a 100644 --- a/spec/commands/feeds/import_from_opml_spec.rb +++ b/spec/commands/feeds/import_from_opml_spec.rb @@ -53,8 +53,8 @@ def import it "sets group_id for existing feeds" do described_class.import(subscriptions) - expect(feed1.reload.group).to eq group1 - expect(feed2.reload.group).to eq group2 + expect(feed1.reload.group).to eq(group1) + expect(feed2.reload.group).to eq(group2) end end @@ -86,8 +86,8 @@ def import it "sets group" do described_class.import(subscriptions) - expect(feed1.first.group).to eq group1 - expect(feed2.first.group).to eq group2 + expect(feed1.first.group).to eq(group1) + expect(feed2.first.group).to eq(group2) end it "does not create empty group" do @@ -104,7 +104,7 @@ def import it "does not create any new group for feeds without group" do described_class.import(subscriptions) - expect(Group.where.not(id: [group1.id, group2.id]).count).to eq 0 + expect(Group.where.not(id: [group1.id, group2.id]).count).to eq(0) end it "creates feeds without group_id" do diff --git a/spec/commands/find_new_stories_spec.rb b/spec/commands/find_new_stories_spec.rb index 903c3447f..4ae39085b 100644 --- a/spec/commands/find_new_stories_spec.rb +++ b/spec/commands/find_new_stories_spec.rb @@ -32,7 +32,7 @@ .to receive(:exists?).with("story2", 1).and_return(false) result = described_class.new(feed, 1, Time.new(2013, 1, 2)).new_stories - expect(result).to eq [story2] + expect(result).to eq([story2]) end end @@ -47,7 +47,7 @@ Time.new(2013, 1, 3), "old-story" ).new_stories - expect(result).to eq [new_story] + expect(result).to eq([new_story]) end it "ignores stories older than 3 days" do diff --git a/spec/commands/users/change_user_password_spec.rb b/spec/commands/users/change_user_password_spec.rb index f109d666b..6c89be12e 100644 --- a/spec/commands/users/change_user_password_spec.rb +++ b/spec/commands/users/change_user_password_spec.rb @@ -20,7 +20,7 @@ command = described_class.new(repo) result = command.change_user_password(new_password) - expect(BCrypt::Password.new(result.password_digest)).to eq new_password + expect(BCrypt::Password.new(result.password_digest)).to eq(new_password) end it "changes the API key of the user" do @@ -30,7 +30,7 @@ command = described_class.new(repo) result = command.change_user_password(new_password) - expect(result.api_key).to eq ApiKey.compute(new_password) + expect(result.api_key).to eq(ApiKey.compute(new_password)) end end end diff --git a/spec/commands/users/sign_in_user_spec.rb b/spec/commands/users/sign_in_user_spec.rb index e5dbc0137..e87cd515d 100644 --- a/spec/commands/users/sign_in_user_spec.rb +++ b/spec/commands/users/sign_in_user_spec.rb @@ -16,7 +16,7 @@ it "returns the user if the password is valid" do result = described_class.sign_in(valid_password, repo) - expect(result.id).to eq 1 + expect(result.id).to eq(1) end it "returns nil if password is invalid" do From bae43012ca329985f94128e82837dfc5ca3e6522 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 17:28:44 -0800 Subject: [PATCH 0495/1107] RuboCop: add parens for config files (#802) --- .rubocop_todo.yml | 2 -- config/asset_pipeline.rb | 36 ++++++++++++++++++++---------------- config/puma.rb | 2 +- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 58db54f8f..886c90866 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -360,8 +360,6 @@ Style/FetchEnvVar: # SupportedStyles: require_parentheses, omit_parentheses Style/MethodCallWithArgsParentheses: Exclude: - - 'config/asset_pipeline.rb' - - 'config/puma.rb' - 'db/migrate/20130409010818_create_feeds.rb' - 'db/migrate/20130409010826_create_stories.rb' - 'db/migrate/20130412185253_add_new_fields_to_stories.rb' diff --git a/config/asset_pipeline.rb b/config/asset_pipeline.rb index 569219604..406b79dd7 100644 --- a/config/asset_pipeline.rb +++ b/config/asset_pipeline.rb @@ -2,10 +2,10 @@ module AssetPipeline def registered(app) - app.set :sprockets, Sprockets::Environment.new(app.root) + app.set(:sprockets, Sprockets::Environment.new(app.root)) ["assets", "stylesheets", "javascripts"].each do |path| - app.get "/#{path}/*" do + app.get("/#{path}/*") do env["PATH_INFO"].sub!(%r{^/#{path}}, "") settings.sprockets.call(env) end @@ -20,29 +20,33 @@ def registered(app) private def append_paths(app) - app.sprockets.append_path File.join(app.root, "app", "assets") - app.sprockets.append_path File.join( - app.root, - "app", - "assets", - "stylesheets" + app.sprockets.append_path(File.join(app.root, "app", "assets")) + app.sprockets.append_path( + File.join( + app.root, + "app", + "assets", + "stylesheets" + ) ) - app.sprockets.append_path File.join( - app.root, - "app", - "assets", - "javascripts" + app.sprockets.append_path( + File.join( + app.root, + "app", + "assets", + "javascripts" + ) ) end def configure_development(app) - app.configure :development, :test do + app.configure(:development, :test) do app.sprockets.cache = Sprockets::Cache::FileStore.new("./tmp") end end def configure_production(app) - app.configure :production, :test do + app.configure(:production, :test) do app.sprockets.css_compressor = :scss app.sprockets.js_compressor = :uglify end @@ -56,7 +60,7 @@ def register_helpers(app) config.digest = true if app.production? end - app.helpers Sprockets::Helpers + app.helpers(Sprockets::Helpers) end module_function :registered, diff --git a/config/puma.rb b/config/puma.rb index e439e2ca5..c36d06d49 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -workers workers Integer(ENV.fetch("WEB_CONCURRENCY", 1)) +workers Integer(ENV.fetch("WEB_CONCURRENCY", 1)) threads_count = Integer(ENV.fetch("MAX_THREADS", 2)) threads threads_count, threads_count From 744c96bc9c7b1e32ac6faf6e87216a408f7b2c05 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 17:32:50 -0800 Subject: [PATCH 0496/1107] RuboCop: add parens for remaining specs (#803) --- .rubocop_todo.yml | 11 ---------- spec/fever_api_spec.rb | 4 ++-- spec/helpers/url_helpers_spec.rb | 14 ++++++------- spec/integration/feed_importing_spec.rb | 4 ++-- spec/models/story_spec.rb | 4 ++-- spec/repositories/feed_repository_spec.rb | 14 ++++++------- spec/repositories/story_repository_spec.rb | 18 ++++++++-------- spec/spec_helper.rb | 4 ++-- spec/utils/content_sanitizer_spec.rb | 8 ++++---- spec/utils/feed_discovery_spec.rb | 4 ++-- spec/utils/i18n_support_spec.rb | 8 ++++---- spec/utils/opml_parser_spec.rb | 24 +++++++++++----------- 12 files changed, 53 insertions(+), 64 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 886c90866..a7b450f66 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -379,17 +379,6 @@ Style/MethodCallWithArgsParentheses: - 'db/migrate/20130805113712_update_stories_unique_constraints.rb' - 'db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb' - 'db/migrate/20140413100725_add_groups_table_and_foreign_keys_to_feeds.rb' - - 'spec/fever_api_spec.rb' - - 'spec/helpers/url_helpers_spec.rb' - - 'spec/integration/feed_importing_spec.rb' - - 'spec/models/story_spec.rb' - - 'spec/repositories/feed_repository_spec.rb' - - 'spec/repositories/story_repository_spec.rb' - - 'spec/spec_helper.rb' - - 'spec/utils/content_sanitizer_spec.rb' - - 'spec/utils/feed_discovery_spec.rb' - - 'spec/utils/i18n_support_spec.rb' - - 'spec/utils/opml_parser_spec.rb' # Offense count: 10 # This cop supports safe autocorrection (--autocorrect). diff --git a/spec/fever_api_spec.rb b/spec/fever_api_spec.rb index 88df51341..9d3715b6a 100644 --- a/spec/fever_api_spec.rb +++ b/spec/fever_api_spec.rb @@ -55,7 +55,7 @@ def last_response_as_object describe "#get" do def make_request(extra_headers = {}) - get "/", headers.merge(extra_headers) + get("/", headers.merge(extra_headers)) end it "returns standard answer" do @@ -191,7 +191,7 @@ def make_request(extra_headers = {}) describe "#post" do def make_request(extra_headers = {}) - post "/", headers.merge(extra_headers) + post("/", headers.merge(extra_headers)) end it "commands to mark story as read" do diff --git a/spec/helpers/url_helpers_spec.rb b/spec/helpers/url_helpers_spec.rb index 8e8a7a5b3..0c7024c30 100644 --- a/spec/helpers/url_helpers_spec.rb +++ b/spec/helpers/url_helpers_spec.rb @@ -14,7 +14,7 @@ it "preserves existing absolute urls" do content = 'bar' - expect(helper.expand_absolute_urls(content, nil)).to eq content + expect(helper.expand_absolute_urls(content, nil)).to eq(content) end it "replaces relative urls in a, img and video tags" do @@ -27,7 +27,7 @@ HTML result = helper.expand_absolute_urls(content, "http://oodl.io/d/") - expect(result.delete("\n")).to eq <<~HTML.delete("\n") + expect(result.delete("\n")).to eq(<<~HTML.delete("\n"))
    tee @@ -38,7 +38,7 @@ end it "handles empty body" do - expect(helper.expand_absolute_urls("", nil)).to eq "" + expect(helper.expand_absolute_urls("", nil)).to eq("") end it "doesn't modify tags that do not have url attributes" do @@ -51,7 +51,7 @@ HTML result = helper.expand_absolute_urls(content, "http://oodl.io/d/") - expect(result.delete("\n")).to eq <<~HTML.delete("\n") + expect(result.delete("\n")).to eq(<<~HTML.delete("\n"))
    @@ -84,7 +84,7 @@ url = helper.normalize_url("//blog.golang.org/context", feed_url) - expect(url).to eq "#{scheme}://blog.golang.org/context" + expect(url).to eq("#{scheme}://blog.golang.org/context") end end @@ -100,7 +100,7 @@ url = helper.normalize_url( "//blog.golang.org/context", "//blog.golang.org/feed.atom" ) - expect(url).to eq "http://blog.golang.org/context" + expect(url).to eq("http://blog.golang.org/context") end it "resolves relative urls" do @@ -108,7 +108,7 @@ "/progrium/dokku/releases/tag/v0.4.4", "https://github.com/progrium/dokku/releases.atom" ) - expect(url).to eq "https://github.com/progrium/dokku/releases/tag/v0.4.4" + expect(url).to eq("https://github.com/progrium/dokku/releases/tag/v0.4.4") end end end diff --git a/spec/integration/feed_importing_spec.rb b/spec/integration/feed_importing_spec.rb index d6227295b..cd4033906 100644 --- a/spec/integration/feed_importing_spec.rb +++ b/spec/integration/feed_importing_spec.rb @@ -22,7 +22,7 @@ before do # articles older than 3 days are ignored, so freeze time within # applicable range of the stories in the sample feed - Timecop.freeze Time.parse("2014-08-15T17:30:00Z") + Timecop.freeze(Time.parse("2014-08-15T17:30:00Z")) end describe "Importing for the first time" do @@ -56,7 +56,7 @@ end describe "Feed with incorrect pubdates" do - before { Timecop.freeze Time.parse("2014-08-12T17:30:00Z") } + before { Timecop.freeze(Time.parse("2014-08-12T17:30:00Z")) } context "has been fetched before" do it "imports all new stories" do diff --git a/spec/models/story_spec.rb b/spec/models/story_spec.rb index 262f0ba34..43f1b78d0 100644 --- a/spec/models/story_spec.rb +++ b/spec/models/story_spec.rb @@ -25,7 +25,7 @@ it "strips html out" do story.title = "Super cool stuff" - expect(story.headline).to eq "Super cool stuff" + expect(story.headline).to eq("Super cool stuff") end end @@ -36,7 +36,7 @@ it "strips html out" do story.body = "Yo dawg" - expect(story.lead).to eq "Yo dawg" + expect(story.lead).to eq("Yo dawg") end end diff --git a/spec/repositories/feed_repository_spec.rb b/spec/repositories/feed_repository_spec.rb index fbcbaef5d..fab1522bc 100644 --- a/spec/repositories/feed_repository_spec.rb +++ b/spec/repositories/feed_repository_spec.rb @@ -19,7 +19,7 @@ result = described_class.fetch(feed.id) - expect(result).to eq feed + expect(result).to eq(feed) end end @@ -44,8 +44,8 @@ described_class.update_feed(feed, "Test Feed", "example.com/feed") - expect(feed.name).to eq "Test Feed" - expect(feed.url).to eq "example.com/feed" + expect(feed.name).to eq("Test Feed") + expect(feed.url).to eq("example.com/feed") end end @@ -57,7 +57,7 @@ described_class.update_last_fetched(feed, timestamp) - expect(feed.last_fetched).to eq timestamp + expect(feed.last_fetched).to eq(timestamp) end it "rejects weird timestamps" do @@ -66,7 +66,7 @@ described_class.update_last_fetched(feed, weird_timestamp) - expect(feed.last_fetched).to eq timestamp + expect(feed.last_fetched).to eq(timestamp) end it "doesn't update if timestamp is nil" do @@ -74,7 +74,7 @@ described_class.update_last_fetched(feed, nil) - expect(feed.last_fetched).to eq timestamp + expect(feed.last_fetched).to eq(timestamp) end it "doesn't update if timestamp is older than the current value" do @@ -83,7 +83,7 @@ described_class.update_last_fetched(feed, one_week_ago) - expect(feed.last_fetched).to eq timestamp + expect(feed.last_fetched).to eq(timestamp) end end diff --git a/spec/repositories/story_repository_spec.rb b/spec/repositories/story_repository_spec.rb index cc50b3046..c03e787bb 100644 --- a/spec/repositories/story_repository_spec.rb +++ b/spec/repositories/story_repository_spec.rb @@ -406,14 +406,14 @@ feed = double(url: "http://github.com") entry = double(url: "https://github.com/stringer-rss/stringer") - expect(described_class.extract_url(entry, feed)).to eq "https://github.com/stringer-rss/stringer" + expect(described_class.extract_url(entry, feed)).to eq("https://github.com/stringer-rss/stringer") end it "returns the enclosure_url when the url is nil" do feed = double(url: "http://github.com") entry = double(url: nil, enclosure_url: "https://github.com/stringer-rss/stringer") - expect(described_class.extract_url(entry, feed)).to eq "https://github.com/stringer-rss/stringer" + expect(described_class.extract_url(entry, feed)).to eq("https://github.com/stringer-rss/stringer") end it "does not crash if url is nil but enclosure_url does not exist" do @@ -428,13 +428,13 @@ it "returns the title if there is a title" do entry = double(title: "title", summary: "summary") - expect(described_class.extract_title(entry)).to eq "title" + expect(described_class.extract_title(entry)).to eq("title") end it "returns the summary if there isn't a title" do entry = double(title: "", summary: "summary") - expect(described_class.extract_title(entry)).to eq "summary" + expect(described_class.extract_title(entry)).to eq("summary") end end @@ -455,18 +455,18 @@ end it "sanitizes content" do - expect(described_class.extract_content(entry)).to eq "Some test content" + expect(described_class.extract_content(entry)).to eq("Some test content") end it "falls back to summary if there is no content" do expect(described_class.extract_content(summary_only)) - .to eq "Dumb publisher" + .to eq("Dumb publisher") end it "returns empty string if there is no content or summary" do entry = double(url: "http://mdswanson.com", content: nil, summary: nil) - expect(described_class.extract_content(entry)).to eq "" + expect(described_class.extract_content(entry)).to eq("") end it "expands urls" do @@ -477,7 +477,7 @@ ) expect(described_class.extract_content(entry)) - .to eq "Page" + .to eq("Page") end it "ignores URL expansion if entry url is nil" do @@ -485,7 +485,7 @@ double(url: nil, content: nil, summary: "Page") expect(described_class.extract_content(entry)) - .to eq "Page" + .to eq("Page") end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 66d5bbe7f..1b1ec9a32 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -36,8 +36,8 @@ def custom_request(method, path, params = {}, env = {}, &) end RSpec.configure do |config| - config.include Rack::Test::Methods - config.include RSpecHtmlMatchers + config.include(Rack::Test::Methods) + config.include(RSpecHtmlMatchers) end def app_require(file) diff --git a/spec/utils/content_sanitizer_spec.rb b/spec/utils/content_sanitizer_spec.rb index 7d4a598f1..03eda1ded 100644 --- a/spec/utils/content_sanitizer_spec.rb +++ b/spec/utils/content_sanitizer_spec.rb @@ -11,25 +11,25 @@ result = described_class.sanitize("WM_ERROR asdf") - expect(result).to eq "WM_ERROR asdf" + expect(result).to eq("WM_ERROR asdf") end it "handles
    tag properly" do result = described_class.sanitize("
    some code
    ") - expect(result).to eq "
    some code
    " + expect(result).to eq("
    some code
    ") end it "handles unprintable characters" do result = described_class.sanitize("n\u2028\u2029") - expect(result).to eq "n" + expect(result).to eq("n") end it "preserves line endings" do result = described_class.sanitize("test\r\ncase") - expect(result).to eq "test\r\ncase" + expect(result).to eq("test\r\ncase") end end end diff --git a/spec/utils/feed_discovery_spec.rb b/spec/utils/feed_discovery_spec.rb index fbf381b04..c6b27291f 100644 --- a/spec/utils/feed_discovery_spec.rb +++ b/spec/utils/feed_discovery_spec.rb @@ -31,7 +31,7 @@ result = described_class.new.discover(url, finder, parser, client) - expect(result).to eq feed + expect(result).to eq(feed) end it "returns false if the discovered feed is not parsable" do @@ -59,7 +59,7 @@ result = described_class.new.discover(url, finder, parser, client) - expect(result).to eq feed + expect(result).to eq(feed) end end end diff --git a/spec/utils/i18n_support_spec.rb b/spec/utils/i18n_support_spec.rb index d469036a7..791bcaa4c 100644 --- a/spec/utils/i18n_support_spec.rb +++ b/spec/utils/i18n_support_spec.rb @@ -13,7 +13,7 @@ let(:locale) { nil } it "loads default locale" do - expect(I18n.locale.to_s).to eq "en" + expect(I18n.locale.to_s).to eq("en") expect(I18n.locale.to_s).not_to be_nil end end @@ -22,8 +22,8 @@ let(:locale) { "en" } it "loads default locale" do - expect(I18n.locale.to_s).to eq "en" - expect(I18n.t("layout.title")).to eq "stringer | your rss buddy" + expect(I18n.locale.to_s).to eq("en") + expect(I18n.t("layout.title")).to eq("stringer | your rss buddy") end end @@ -32,7 +32,7 @@ it "does not find localization strings" do expect(I18n.t("layout.title", locale: ENV["LOCALE"].to_sym)) - .not_to eq "stringer | your rss buddy" + .not_to eq("stringer | your rss buddy") end end end diff --git a/spec/utils/opml_parser_spec.rb b/spec/utils/opml_parser_spec.rb index 4520a8a07..90e5afc55 100644 --- a/spec/utils/opml_parser_spec.rb +++ b/spec/utils/opml_parser_spec.rb @@ -25,13 +25,13 @@ XML resulted_values = result.values.flatten - expect(resulted_values.size).to eq 2 - expect(resulted_values.first[:name]).to eq "a sample feed" - expect(resulted_values.first[:url]).to eq "http://feeds.feedburner.com/foobar" + expect(resulted_values.size).to eq(2) + expect(resulted_values.first[:name]).to eq("a sample feed") + expect(resulted_values.first[:url]).to eq("http://feeds.feedburner.com/foobar") - expect(resulted_values.last[:name]).to eq "Matt's Blog" - expect(resulted_values.last[:url]).to eq "http://mdswanson.com/atom.xml" - expect(result.keys.first).to eq "Ungrouped" + expect(resulted_values.last[:name]).to eq("Matt's Blog") + expect(resulted_values.last[:url]).to eq("http://mdswanson.com/atom.xml") + expect(result.keys.first).to eq("Ungrouped") end it "handles nested groups of feeds" do @@ -51,10 +51,10 @@ XML resulted_values = result.values.flatten - expect(resulted_values.count).to eq 1 - expect(resulted_values.first[:name]).to eq "a sample feed" - expect(resulted_values.first[:url]).to eq "http://feeds.feedburner.com/foobar" - expect(result.keys.first).to eq "Technology News" + expect(resulted_values.count).to eq(1) + expect(resulted_values.first[:name]).to eq("a sample feed") + expect(resulted_values.first[:url]).to eq("http://feeds.feedburner.com/foobar") + expect(result.keys.first).to eq("Technology News") end it "doesn't explode when there are no feeds" do @@ -87,8 +87,8 @@ XML resulted_values = result.values.flatten - expect(resulted_values.count).to eq 1 - expect(resulted_values.first[:name]).to eq "a sample feed" + expect(resulted_values.count).to eq(1) + expect(resulted_values.first[:name]).to eq("a sample feed") end end end From 1667aae1971edb30fd5685b68ed4de33ac94218a Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Tue, 3 Jan 2023 17:37:55 -0800 Subject: [PATCH 0497/1107] RuboCop: exclude db directory for parens linter (#804) --- .rubocop.yml | 2 ++ .rubocop_todo.yml | 26 -------------------------- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 5e7e26fad..9e1bdc7db 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -24,6 +24,8 @@ Style/MethodCallWithArgsParentheses: - describe - require - task + Exclude: + - db/**/*.rb Style/StringLiterals: { EnforcedStyle: double_quotes } Style/SymbolArray: { EnforcedStyle: brackets } Style/WordArray: { EnforcedStyle: brackets } diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index a7b450f66..45b1bb5ea 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -354,32 +354,6 @@ Style/FetchEnvVar: Exclude: - 'Rakefile' -# Offense count: 184 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: IgnoreMacros, AllowedMethods, IgnoredMethods, AllowedPatterns, IgnoredPatterns, IncludedMacros, AllowParenthesesInMultilineCall, AllowParenthesesInChaining, AllowParenthesesInCamelCaseMethod, AllowParenthesesInStringInterpolation, EnforcedStyle. -# SupportedStyles: require_parentheses, omit_parentheses -Style/MethodCallWithArgsParentheses: - Exclude: - - 'db/migrate/20130409010818_create_feeds.rb' - - 'db/migrate/20130409010826_create_stories.rb' - - 'db/migrate/20130412185253_add_new_fields_to_stories.rb' - - 'db/migrate/20130418221144_add_user_model.rb' - - 'db/migrate/20130423001740_drop_email_from_user.rb' - - 'db/migrate/20130423180446_remove_author_from_stories.rb' - - 'db/migrate/20130425211008_add_setup_complete_to_user.rb' - - 'db/migrate/20130425222157_add_delayed_job.rb' - - 'db/migrate/20130429232127_add_status_to_feeds.rb' - - 'db/migrate/20130504005816_text_url.rb' - - 'db/migrate/20130504022615_change_story_permalink_column.rb' - - 'db/migrate/20130509131045_add_unique_constraints.rb' - - 'db/migrate/20130513025939_add_keep_unread_to_stories.rb' - - 'db/migrate/20130513044029_add_is_starred_status_for_stories.rb' - - 'db/migrate/20130522014405_add_api_key_to_user.rb' - - 'db/migrate/20130730120312_add_entry_id_to_stories.rb' - - 'db/migrate/20130805113712_update_stories_unique_constraints.rb' - - 'db/migrate/20130905204142_use_text_datatype_for_title_and_entry_id.rb' - - 'db/migrate/20140413100725_add_groups_table_and_foreign_keys_to_feeds.rb' - # Offense count: 10 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinDigits, Strict, AllowedNumbers, AllowedPatterns. From 69be6f93b335dfcba01c4939220bff8bb28b2fd0 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sun, 8 Jan 2023 16:03:09 -0800 Subject: [PATCH 0498/1107] Rails: introduce config/routes.rb (#805) Move ActionController routes to file that more closely mimics Rails. --- app.rb | 16 +--------------- app/helpers/controller_helpers.rb | 18 ------------------ config/routes.rb | 26 ++++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 33 deletions(-) delete mode 100644 app/helpers/controller_helpers.rb create mode 100644 config/routes.rb diff --git a/app.rb b/app.rb index d74d6df97..608ec2881 100644 --- a/app.rb +++ b/app.rb @@ -17,7 +17,6 @@ require "securerandom" require_relative "app/helpers/authentication_helpers" -require_relative "app/helpers/controller_helpers" require_relative "app/repositories/user_repository" require_relative "config/asset_pipeline" @@ -44,8 +43,6 @@ class Stringer < Sinatra::Base use Rack::SSL, exclude: ->(env) { env["PATH_INFO"] =~ %r{^/(js|css|img)} } end - extend Sinatra::ControllerHelpers - register Sinatra::ActiveRecordExtension register Sinatra::Flash register Sinatra::Contrib @@ -101,20 +98,9 @@ def t(*args, **kwargs) redirect to("/setup/password") end end - - rails_route(:get, "/debug", to: "debug#index") - rails_route(:get, "/heroku", to: "debug#heroku") - rails_route(:get, "/feeds", to: "feeds#index") - rails_route(:get, "/feeds/:id/edit", to: "feeds#edit") - rails_route(:put, "/feeds/:id", to: "feeds#update") - rails_route(:delete, "/feeds/:id", to: "feeds#destroy") - rails_route(:get, "/feeds/new", to: "feeds#new") - rails_route(:post, "/feeds", to: "feeds#create") - rails_route(:get, "/feeds/export", to: "exports#index") - rails_route(:get, "/feeds/import", to: "imports#new") - rails_route(:post, "/feeds/import", to: "imports#create") end require_relative "app/controllers/sinatra/stories_controller" require_relative "app/controllers/sinatra/first_run_controller" require_relative "app/controllers/sinatra/sessions_controller" +require_relative "config/routes" diff --git a/app/helpers/controller_helpers.rb b/app/helpers/controller_helpers.rb deleted file mode 100644 index 4070c9c0e..000000000 --- a/app/helpers/controller_helpers.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Sinatra - module ControllerHelpers - def rails_route(method, path, options) - options = options.with_indifferent_access - to = options.delete(:to) - controller_name, action_name = to.split("#") - controller_klass = "#{controller_name.camelize}Controller".constantize - route(method.to_s.upcase, path, options) do - # Make sure that our parsed URL params are where Rack (and - # ActionDispatch) expect them - app = controller_klass.action(action_name) - app.call(request.env.merge("rack.request.query_hash" => params)) - end - end - end -end diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 000000000..c3f1d93bb --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class Stringer < Sinatra::Base + def self.match(path, to:, via:) + controller_name, action_name = to.split("#") + controller_klass = "#{controller_name.camelize}Controller".constantize + route(via.to_s.upcase, path) do + # Make sure that our parsed URL params are where Rack (and + # ActionDispatch) expect them + app = controller_klass.action(action_name) + app.call(request.env.merge("rack.request.query_hash" => params)) + end + end + + match("/debug", to: "debug#index", via: :get) + match("/heroku", to: "debug#heroku", via: :get) + match("/feeds", to: "feeds#index", via: :get) + match("/feeds/:id/edit", to: "feeds#edit", via: :get) + match("/feeds/:id", to: "feeds#update", via: :put) + match("/feeds/:id", to: "feeds#destroy", via: :delete) + match("/feeds/new", to: "feeds#new", via: :get) + match("/feeds", to: "feeds#create", via: :post) + match("/feeds/export", to: "exports#index", via: :get) + match("/feeds/import", to: "imports#new", via: :get) + match("/feeds/import", to: "imports#create", via: :post) +end From a076213c32576745cd07f189ae71d10cb69dcdbf Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Mon, 9 Jan 2023 17:00:45 -0800 Subject: [PATCH 0499/1107] Update all Bundler dependencies (2023-01-09) (#806) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9a1cbe79e..cbd0fd2d8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -214,13 +214,13 @@ GEM rspec-mocks (~> 3.12.0) rspec-core (3.12.0) rspec-support (~> 3.12.0) - rspec-expectations (3.12.1) + rspec-expectations (3.12.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-html-matchers (0.10.0) nokogiri (~> 1) rspec (>= 3.0.0.a) - rspec-mocks (3.12.1) + rspec-mocks (3.12.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-rails (6.0.1) @@ -303,7 +303,7 @@ GEM concurrent-ruby (~> 1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) - unicode-display_width (2.4.1) + unicode-display_width (2.4.2) webmock (3.18.1) addressable (>= 2.8.0) crack (>= 0.3.2) From 30e50a3ea3cfa71976f4381dcefae3c44044468d Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 14 Jan 2023 12:14:02 -0800 Subject: [PATCH 0500/1107] Rails: move password setup to new controller (#807) --- .rubocop_todo.yml | 1 + app.rb | 1 + app/controllers/passwords_controller.rb | 33 +++++++++++++ .../sinatra/first_run_controller.rb | 30 +----------- .../password.erb => passwords/new.html.erb} | 0 config/routes.rb | 2 + spec/controllers/first_run_controller_spec.rb | 42 ----------------- spec/controllers/passwords_controller_spec.rb | 47 +++++++++++++++++++ spec/support/coverage.rb | 2 +- 9 files changed, 86 insertions(+), 72 deletions(-) create mode 100644 app/controllers/passwords_controller.rb rename app/views/{first_run/password.erb => passwords/new.html.erb} (100%) create mode 100644 spec/controllers/passwords_controller_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 45b1bb5ea..7e3578342 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -255,6 +255,7 @@ Rails/HttpPositionalArguments: - 'spec/controllers/feeds_controller_spec.rb' - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/imports_controller_spec.rb' + - 'spec/controllers/passwords_controller_spec.rb' - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' - 'spec/fever_api_spec.rb' diff --git a/app.rb b/app.rb index 608ec2881..d7501492e 100644 --- a/app.rb +++ b/app.rb @@ -25,6 +25,7 @@ require_relative "app/controllers/feeds_controller" require_relative "app/controllers/exports_controller" require_relative "app/controllers/imports_controller" +require_relative "app/controllers/passwords_controller" module Rails def self.application diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 000000000..c71936bd1 --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class PasswordsController < ApplicationController + before_action :redirect_if_setup_complete + + def new; end + + def create + if no_password(params) || password_mismatch?(params) + flash.now[:error] = t("first_run.password.flash.passwords_dont_match") + render(:new) + else + user = CreateUser.call(params[:password]) + session[:user_id] = user.id + + redirect_to("/feeds/import") + end + end + + private + + def no_password(params) + params[:password].nil? || params[:password] == "" + end + + def password_mismatch?(params) + params[:password] != params[:password_confirmation] + end + + def redirect_if_setup_complete + redirect_to("/news") if UserRepository.setup_complete? + end +end diff --git a/app/controllers/sinatra/first_run_controller.rb b/app/controllers/sinatra/first_run_controller.rb index 8556addc6..3ca3f5810 100644 --- a/app/controllers/sinatra/first_run_controller.rb +++ b/app/controllers/sinatra/first_run_controller.rb @@ -9,27 +9,9 @@ class Stringer < Sinatra::Base namespace "/setup" do - before do + get "/tutorial" do redirect to("/news") if UserRepository.setup_complete? - end - - get "/password" do - erb :"first_run/password" - end - - post "/password" do - if no_password(params) || password_mismatch?(params) - flash.now[:error] = t("first_run.password.flash.passwords_dont_match") - erb :"first_run/password" - else - user = CreateUser.call(params[:password]) - session[:user_id] = user.id - - redirect to("/feeds/import") - end - end - get "/tutorial" do FetchFeeds.enqueue(Feed.all) CompleteSetup.complete(current_user) @@ -37,14 +19,4 @@ class Stringer < Sinatra::Base erb :tutorial end end - - private - - def no_password(params) - params[:password].nil? || params[:password] == "" - end - - def password_mismatch?(params) - params[:password] != params[:password_confirmation] - end end diff --git a/app/views/first_run/password.erb b/app/views/passwords/new.html.erb similarity index 100% rename from app/views/first_run/password.erb rename to app/views/passwords/new.html.erb diff --git a/config/routes.rb b/config/routes.rb index c3f1d93bb..0ec5cb3a9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -23,4 +23,6 @@ def self.match(path, to:, via:) match("/feeds/export", to: "exports#index", via: :get) match("/feeds/import", to: "imports#new", via: :get) match("/feeds/import", to: "imports#create", via: :post) + match("/setup/password", to: "passwords#new", via: :get) + match("/setup/password", to: "passwords#create", via: :post) end diff --git a/spec/controllers/first_run_controller_spec.rb b/spec/controllers/first_run_controller_spec.rb index bf9e4465a..9ac408c0f 100644 --- a/spec/controllers/first_run_controller_spec.rb +++ b/spec/controllers/first_run_controller_spec.rb @@ -7,48 +7,6 @@ describe "FirstRunController" do context "when a user has not been setup" do - def setup - expect(UserRepository) - .to receive(:setup_complete?).twice.and_return(false) - end - - describe "GET /setup/password" do - it "displays a form to enter your password" do - setup - - get "/setup/password" - - page = last_response.body - expect(page).to have_tag("form#password_setup") - end - end - - describe "POST /setup/password" do - it "rejects empty passwords" do - setup - - post "/setup/password" - - page = last_response.body - expect(page).to have_tag("div.error") - end - - it "rejects when password isn't confirmed" do - setup - - post "/setup/password", password: "foo", password_confirmation: "bar" - - page = last_response.body - expect(page).to have_tag("div.error") - end - - it "accepts confirmed passwords and redirects to next step" do - post "/setup/password", password: "foo", password_confirmation: "foo" - - expect(URI.parse(last_response.location).path).to eq("/feeds/import") - end - end - describe "GET /setup/tutorial" do let(:user) { instance_double(User) } let(:feeds) { [instance_double(Feed), instance_double(Feed)] } diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb new file mode 100644 index 000000000..12e45d913 --- /dev/null +++ b/spec/controllers/passwords_controller_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "spec_helper" +require "support/active_record" + +RSpec.describe PasswordsController do + def setup + expect(UserRepository).to receive(:setup_complete?).twice.and_return(false) + end + + describe "#new" do + it "displays a form to enter your password" do + setup + + get "/setup/password" + + page = last_response.body + expect(page).to have_tag("form#password_setup") + end + end + + describe "#create" do + it "rejects empty passwords" do + setup + + post "/setup/password" + + page = last_response.body + expect(page).to have_tag("div.error") + end + + it "rejects when password isn't confirmed" do + setup + + post "/setup/password", password: "foo", password_confirmation: "bar" + + page = last_response.body + expect(page).to have_tag("div.error") + end + + it "accepts confirmed passwords and redirects to next step" do + post "/setup/password", password: "foo", password_confirmation: "foo" + + expect(URI.parse(last_response.location).path).to eq("/feeds/import") + end + end +end diff --git a/spec/support/coverage.rb b/spec/support/coverage.rb index 6c914f365..29cf4d5dc 100644 --- a/spec/support/coverage.rb +++ b/spec/support/coverage.rb @@ -15,4 +15,4 @@ add_group("Utils", "app/utils") enable_coverage :branch end -SimpleCov.minimum_coverage(line: 100, branch: 99) +SimpleCov.minimum_coverage(line: 100, branch: 98) From 1f78dc4f67fedd2d9e208eeb869c76c6ed458c64 Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Sat, 14 Jan 2023 13:41:13 -0800 Subject: [PATCH 0501/1107] Rails: move tutorials to ApplicationController (#808) The newly added views are all copies of existing partials. The old ones will be removed when all of the references are gone. --- .rubocop_todo.yml | 2 +- app.rb | 7 ++- .../sinatra/first_run_controller.rb | 22 -------- app/controllers/tutorials_controller.rb | 15 ++++++ app/views/feeds/index.html.erb | 2 +- app/views/stories/_js.html.erb | 43 +++++++++++++++ .../stories/_mark_all_as_read_form.html.erb | 7 +++ app/views/stories/_templates.html.erb | 54 +++++++++++++++++++ .../_action_bar.html.erb} | 3 +- .../index.html.erb} | 4 +- config/routes.rb | 1 + spec/controllers/first_run_controller_spec.rb | 42 --------------- spec/controllers/tutorials_controller_spec.rb | 40 ++++++++++++++ 13 files changed, 171 insertions(+), 71 deletions(-) delete mode 100644 app/controllers/sinatra/first_run_controller.rb create mode 100644 app/controllers/tutorials_controller.rb create mode 100644 app/views/stories/_js.html.erb create mode 100644 app/views/stories/_mark_all_as_read_form.html.erb create mode 100644 app/views/stories/_templates.html.erb rename app/views/{partials/_tutorial_action_bar.erb => tutorials/_action_bar.html.erb} (87%) rename app/views/{tutorial.erb => tutorials/index.html.erb} (92%) delete mode 100644 spec/controllers/first_run_controller_spec.rb create mode 100644 spec/controllers/tutorials_controller_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 7e3578342..4761ceaed 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -253,11 +253,11 @@ Rails/HttpPositionalArguments: - 'spec/app_spec.rb' - 'spec/controllers/debug_controller_spec.rb' - 'spec/controllers/feeds_controller_spec.rb' - - 'spec/controllers/first_run_controller_spec.rb' - 'spec/controllers/imports_controller_spec.rb' - 'spec/controllers/passwords_controller_spec.rb' - 'spec/controllers/sessions_controller_spec.rb' - 'spec/controllers/stories_controller_spec.rb' + - 'spec/controllers/tutorials_controller_spec.rb' - 'spec/fever_api_spec.rb' # Offense count: 1 diff --git a/app.rb b/app.rb index d7501492e..6fcda39b3 100644 --- a/app.rb +++ b/app.rb @@ -16,8 +16,13 @@ require "sprockets-helpers" require "securerandom" +require_relative "app/commands/feeds/import_from_opml" +require_relative "app/commands/users/complete_setup" +require_relative "app/commands/users/create_user" require_relative "app/helpers/authentication_helpers" +require_relative "app/repositories/story_repository" require_relative "app/repositories/user_repository" +require_relative "app/tasks/fetch_feeds" require_relative "config/asset_pipeline" require_relative "app/controllers/application_controller" @@ -26,6 +31,7 @@ require_relative "app/controllers/exports_controller" require_relative "app/controllers/imports_controller" require_relative "app/controllers/passwords_controller" +require_relative "app/controllers/tutorials_controller" module Rails def self.application @@ -102,6 +108,5 @@ def t(*args, **kwargs) end require_relative "app/controllers/sinatra/stories_controller" -require_relative "app/controllers/sinatra/first_run_controller" require_relative "app/controllers/sinatra/sessions_controller" require_relative "config/routes" diff --git a/app/controllers/sinatra/first_run_controller.rb b/app/controllers/sinatra/first_run_controller.rb deleted file mode 100644 index 3ca3f5810..000000000 --- a/app/controllers/sinatra/first_run_controller.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require_relative "../../commands/feeds/import_from_opml" -require_relative "../../commands/users/create_user" -require_relative "../../commands/users/complete_setup" -require_relative "../../repositories/user_repository" -require_relative "../../repositories/story_repository" -require_relative "../../tasks/fetch_feeds" - -class Stringer < Sinatra::Base - namespace "/setup" do - get "/tutorial" do - redirect to("/news") if UserRepository.setup_complete? - - FetchFeeds.enqueue(Feed.all) - CompleteSetup.complete(current_user) - - @sample_stories = StoryRepository.samples - erb :tutorial - end - end -end diff --git a/app/controllers/tutorials_controller.rb b/app/controllers/tutorials_controller.rb new file mode 100644 index 000000000..ee68709ea --- /dev/null +++ b/app/controllers/tutorials_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class TutorialsController < ApplicationController + def index + if UserRepository.setup_complete? + redirect_to("/news") + return + end + + FetchFeeds.enqueue(Feed.all) + CompleteSetup.complete(current_user) + + @sample_stories = StoryRepository.samples + end +end diff --git a/app/views/feeds/index.html.erb b/app/views/feeds/index.html.erb index 7709e4633..79025c862 100644 --- a/app/views/feeds/index.html.erb +++ b/app/views/feeds/index.html.erb @@ -12,7 +12,7 @@
    <% else %>
    -

    <%= t('feeds.index.add_some_feeds', :add => ''+t('feeds.index.add')+'') %>

    +

    <%= t('feeds.index.add_some_feeds', :add => ''+t('feeds.index.add')+'').html_safe %>

    <% end %> diff --git a/app/views/stories/_js.html.erb b/app/views/stories/_js.html.erb new file mode 100644 index 000000000..0ff6d1681 --- /dev/null +++ b/app/views/stories/_js.html.erb @@ -0,0 +1,43 @@ +<%= render 'stories/templates' %> + + diff --git a/app/views/stories/_mark_all_as_read_form.html.erb b/app/views/stories/_mark_all_as_read_form.html.erb new file mode 100644 index 000000000..2e4b3089e --- /dev/null +++ b/app/views/stories/_mark_all_as_read_form.html.erb @@ -0,0 +1,7 @@ +
    +
    + <% stories.each do |story| %> + + <% end %> +
    +
    \ No newline at end of file diff --git a/app/views/stories/_templates.html.erb b/app/views/stories/_templates.html.erb new file mode 100644 index 000000000..2c10c12fc --- /dev/null +++ b/app/views/stories/_templates.html.erb @@ -0,0 +1,54 @@ + diff --git a/app/views/partials/_tutorial_action_bar.erb b/app/views/tutorials/_action_bar.html.erb similarity index 87% rename from app/views/partials/_tutorial_action_bar.erb rename to app/views/tutorials/_action_bar.html.erb index f5ed6d893..7432d1135 100644 --- a/app/views/partials/_tutorial_action_bar.erb +++ b/app/views/tutorials/_action_bar.html.erb @@ -2,8 +2,7 @@
    - <%= render_partial :tutorial_action_bar, {stories: @sample_stories} %> + <%= render 'tutorials/action_bar', {stories: @sample_stories} %>
    -<%= render_js :stories, { stories: @sample_stories } %> +<%= render 'stories/js', { stories: @sample_stories } %>