hnews-suggest 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 073e2495e8923b226a7f5cd9ff08e1794841bfae
4
+ data.tar.gz: f9e7ae657c9937d0dd320c4d52a8b7cb35e8dbb5
5
+ SHA512:
6
+ metadata.gz: 1a940ee93f40a54aa29faf5938ab7a4d1f606451b96f523afe2e7c72961e77dd046fd07fea0f6701885f5dd300bd2216de4fb1a3de5736a1509c23dba17315a8
7
+ data.tar.gz: 37d264566a59c9d372644a2d7cf24f78eafbfd861d0570d887cb3b6a6cd2a2ae736ef594896be1c02ae54fde6a5e07920373c40b1225bf9ee57cc536b9db8464
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ *.sqlite3
2
+ pkg
3
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2014 Greg Chapple
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # HNews Suggest
2
+
3
+ Automatically suggest Hacker News articles from the command line!
4
+
5
+ ## About
6
+
7
+ I am a frequent reader of Hacker News, and thought it would be fun to make something which would suggest an article without having to read through the front page.
8
+
9
+ The more you use the gem, the smarter it gets. After an article is suggested you have the opportunity to feed back into the system. Depending on whether you liked the article or not, the main keywords from the article will be indexed and used for future suggestions.
10
+
11
+ __note__: It's worth mentioning that the method used to rank and suggest articles here is pretty simple. The main keywords are taken from the articles title and content and ranked over time. This can lead to some inaccuracies. In the future, I plan on replacing this system with one based on natural language.
12
+
13
+ ## Installation
14
+
15
+ Installation is super easy! (if you have ruby already installed).
16
+
17
+ ```
18
+ gem install hnews-suggest
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Using the gem is really simple too!
24
+
25
+ ```
26
+ hnews suggest
27
+ ```
28
+
29
+ This command will suggest the top ranked article currently on the Hacker News front page. The article's title and URL will be displayed, followed by a prompt asking for your feedback. If you liked the suggested article, enter `y`. Otherwise enter `n`.
30
+
31
+ You can also pass the `--open` option to the suggest command. This will open the suggested article in your browser.
32
+
33
+ There's also the `learn` command:
34
+
35
+ ```
36
+ hnews learn
37
+ ```
38
+
39
+ This will list all thirty articles currently on the front page, and allow you to pick articles outside of the suggestion system. This is useful if you want to "teach" the system, or if you see an article you like which is not appearing in the suggestion results.
40
+
41
+ ## License
42
+
43
+ Released under the MIT license. See the [LICENSE][] file for further details.
44
+
45
+ [license]: LICENSE
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ namespace :test do
5
+ Rake::TestTask.new do |t|
6
+ t.name = "integration"
7
+ t.pattern = "test/integration/**/*_test.rb"
8
+ t.libs << "test"
9
+ t.libs << "lib"
10
+ t.libs << "config"
11
+ end
12
+
13
+ Rake::TestTask.new do |t|
14
+ t.name = "unit"
15
+ t.pattern = "test/unit/**/*_test.rb"
16
+ t.libs << "test"
17
+ t.libs << "lib"
18
+ t.libs << "config"
19
+ end
20
+ end
21
+
22
+ task test: %w(test:unit test:integration)
23
+ task default: :test
data/bin/hnews ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.expand_path("../../lib", __FILE__)
4
+ $: << File.expand_path("../../config", __FILE__)
5
+ require "fileutils"
6
+
7
+ require "sequel"
8
+ require "thor"
9
+ require "launchy"
10
+
11
+ db_dir = File.expand_path "~/.hnews/"
12
+ db_file = File.join db_dir, "db.sqlite3"
13
+ unless File.exists? db_file
14
+ puts "Creating database file"
15
+ FileUtils.mkdir_p db_dir
16
+
17
+ Sequel.extension :migration
18
+ db = Sequel.connect "sqlite://#{db_file}"
19
+
20
+ root = File.expand_path File.dirname(__FILE__)
21
+ root = File.dirname root
22
+ migrations = File.join root, "db/migrate"
23
+
24
+ puts "Running migrations"
25
+ Sequel::Migrator.run db, migrations
26
+ puts "-------"
27
+ end
28
+
29
+ require "config"
30
+ require "hnews"
31
+
32
+
33
+ HNews::App.start ARGV
data/config/config.rb ADDED
@@ -0,0 +1,7 @@
1
+ if ENV["APP_ENV"]
2
+ Sequel.connect "sqlite://db/#{ENV["APP_ENV"]}.sqlite3"
3
+ else
4
+ db_file = File.expand_path("~/.hnews/db.sqlite3")
5
+
6
+ Sequel.connect "sqlite://#{db_file}"
7
+ end
@@ -0,0 +1,11 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table :articles do
4
+ primary_key :id
5
+ String :title
6
+ String :url
7
+ Boolean :read
8
+ Integer :rank
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table :keywords do
4
+ primary_key :id
5
+ String :word
6
+ Integer :rank
7
+ Time :last_used
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ Sequel.migration do
2
+ change do
3
+ add_column :articles, :content, String
4
+ end
5
+ end
data/hnews.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hnews/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "hnews-suggest"
8
+ spec.version = HNews::VERSION
9
+ spec.authors = ["Greg Chapple"]
10
+ spec.email = ["gregchapple1@gmail.com"]
11
+ spec.summary = %q{Automatically suggest articles from Hacker News}
12
+ spec.description = %q{Suggest articles from Hacker News. Suggestions will get better over time as the gem learns what you like!}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.required_ruby_version = '>= 1.9.3'
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.6"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "factory_girl"
26
+ spec.add_development_dependency "database_cleaner"
27
+ spec.add_development_dependency "activesupport", "4.1.1"
28
+
29
+
30
+ spec.add_dependency "sequel"
31
+ spec.add_dependency "sqlite3"
32
+ spec.add_dependency "nokogiri"
33
+ spec.add_dependency "thor"
34
+ spec.add_dependency "launchy"
35
+ end
data/lib/hnews.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "hnews/services"
2
+ require "hnews/models"
3
+ require "hnews/cli"
data/lib/hnews/cli.rb ADDED
@@ -0,0 +1,59 @@
1
+ module HNews
2
+ class App < Thor
3
+ desc :learn, "Learn what articles you like"
4
+ def learn
5
+ ingest = Ingest.new
6
+ ingest.start
7
+
8
+ list = ListArticles.new
9
+ list.start
10
+
11
+ pick = PickArticle.new
12
+ pick.start || exit
13
+
14
+ scrape = ScrapeArticle.new pick.article
15
+ scrape.start
16
+
17
+ index = IndexArticle.new pick.article
18
+ index.start
19
+ end
20
+
21
+ desc :suggest, "Suggest an article and optionally open in your browser"
22
+ option :open, type: :boolean, banner: "Open the suggested article in your browser"
23
+ def suggest
24
+ ingest = Ingest.new
25
+ ingest.start
26
+
27
+ suggestion = SuggestArticle.new
28
+ suggestion.start
29
+
30
+ if options[:open]
31
+ Launchy.open(suggestion.article.url)
32
+ end
33
+
34
+ $stdout.print "Did you like this article? [y/n] "
35
+ answer = $stdin.gets.chomp.downcase
36
+
37
+ if answer == "y"
38
+ scrape = ScrapeArticle.new suggestion.article
39
+ scrape.start
40
+
41
+ index = IndexArticle.new suggestion.article
42
+ index.start
43
+ elsif answer == "n"
44
+ # todo: perhaps suggest another article here?
45
+ else
46
+ exit
47
+ end
48
+
49
+ suggestion.article.read = true
50
+ suggestion.article.save
51
+ end
52
+
53
+ desc :version, "Output version info"
54
+ def version
55
+ require "hnews/version"
56
+ puts "hnews-suggest version #{HNews::VERSION}"
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,2 @@
1
+ require "hnews/models/article"
2
+ require "hnews/models/keyword"
@@ -0,0 +1,2 @@
1
+ class Article < Sequel::Model
2
+ end
@@ -0,0 +1,2 @@
1
+ class Keyword < Sequel::Model
2
+ end
@@ -0,0 +1,6 @@
1
+ require "hnews/services/index_article"
2
+ require "hnews/services/ingest"
3
+ require "hnews/services/list_articles"
4
+ require "hnews/services/pick_article"
5
+ require "hnews/services/suggest_article"
6
+ require "hnews/services/scrape_article"
@@ -0,0 +1,55 @@
1
+ module HNews
2
+
3
+ END_WORDS = %w(a in is as an if the at of on or i are you re one s ok to too all and that ve so yet be do it for now with me)
4
+
5
+ class IndexArticle
6
+ def initialize article, opts={}
7
+ @article = article
8
+ @input = opts[:input] || $stdin
9
+ @output = opts[:output] || $stdout
10
+ end
11
+
12
+ def start
13
+ index :title
14
+ index :content
15
+ end
16
+
17
+ private
18
+
19
+ def index key
20
+ filter_end_words @article[key]
21
+ generate_keywords
22
+ save key
23
+ render
24
+ end
25
+
26
+ def filter_end_words str
27
+ words = str.split(/\W+/)
28
+ words = words.map{ |word| word.downcase }
29
+ @words = words.select { |word| word unless END_WORDS.include? word }
30
+ @content = @words.join(" ")
31
+ end
32
+
33
+ def generate_keywords
34
+ @words.each do |word|
35
+ keyword = Keyword.first(word: word)
36
+ if keyword.nil?
37
+ keyword = Keyword.new(word: word, rank: 1)
38
+ else
39
+ keyword.rank += 1
40
+ end
41
+ keyword.last_used = Time.now
42
+ keyword.save
43
+ end
44
+ end
45
+
46
+ def save key
47
+ @article[key] = @content
48
+ @article.save
49
+ end
50
+
51
+ def render
52
+ @output.puts "Indexed #{@article.title}"
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,48 @@
1
+ require "nokogiri"
2
+ require "open-uri"
3
+
4
+ module HNews
5
+ class Ingest
6
+ attr_reader :articles
7
+
8
+ def initialize
9
+ @articles = []
10
+ end
11
+
12
+ def start
13
+ scan_page
14
+ get_links
15
+ generate_articles
16
+ end
17
+
18
+ private
19
+
20
+ def scan_page
21
+ page = open("https://news.ycombinator.com")
22
+ @doc = Nokogiri::HTML(page)
23
+ end
24
+
25
+ def get_links
26
+ @links = @doc.css("td.title").select { |td| links td }
27
+ @links = @links.map { |i| i.at("a") }
28
+ end
29
+
30
+ def generate_articles
31
+ @links.each do |link|
32
+ @articles << create_or_get_article(link.content, link[:href])
33
+ end
34
+ end
35
+
36
+ def create_or_get_article title, url
37
+ existing_article = Article.first(url: url)
38
+ return existing_article unless existing_article.nil?
39
+
40
+ article = Article.new title: title, url: url, rank: 0
41
+ article.save
42
+ end
43
+
44
+ def links td
45
+ td[:align] != "right" && td.at("a")[:href] != "news2"
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ module HNews
2
+ class ListArticles
3
+ def initialize options={}
4
+ @input = options[:input] || $stdin
5
+ @output = options[:output] || $stdout
6
+ end
7
+
8
+ def start
9
+ get_articles
10
+ display_articles
11
+ end
12
+
13
+ private
14
+
15
+ def get_articles
16
+ @articles = Article.where(read: nil).limit(30)
17
+ end
18
+
19
+ def display_articles
20
+ @articles.each do |article|
21
+ @output.puts "[#{article.id}] #{article.title}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ module HNews
2
+ class PickArticle
3
+ attr_reader :article
4
+
5
+ def initialize opts={}
6
+ @input = opts[:input] || $stdin
7
+ @output = opts[:output] || $stdout
8
+ end
9
+
10
+ def start
11
+ @output.print "Please enter an id:"
12
+ @id = @input.gets.chomp
13
+ choose
14
+ render
15
+ end
16
+
17
+ private
18
+
19
+ def choose
20
+ @article = Article[@id]
21
+ end
22
+
23
+ def render
24
+ if @article.nil?
25
+ @output.puts "Not a valid id"
26
+ return false
27
+ else
28
+ @output.puts "Picked: #{@article.title}"
29
+ return true
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ require "open-uri"
2
+
3
+ module HNews
4
+ class ScrapeArticle
5
+ def initialize article, opts={}
6
+ @input = opts[:input] || $stdin
7
+ @output = opts[:output] || $stdout
8
+ @article = article
9
+ end
10
+
11
+ def start
12
+ get_content
13
+ end
14
+
15
+ private
16
+
17
+ def get_content
18
+ paras = []
19
+ doc = Nokogiri::HTML(open(@article.url))
20
+ doc.css("p").each do |para|
21
+ paras << para.content
22
+ end
23
+ @article.content = paras.join(" ")
24
+ @article.save
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ module HNews
2
+ class SuggestArticle
3
+ attr_reader :article
4
+
5
+ def initialize opts={}
6
+ @input = opts[:input] || $stdin
7
+ @output = opts[:output] || $stdout
8
+ end
9
+
10
+ def start
11
+ get_articles
12
+ calculate_article_rank
13
+ suggest_articles
14
+ end
15
+
16
+ private
17
+
18
+ def get_articles
19
+ @articles = Article.where(read: nil).exclude(title: nil)
20
+ end
21
+
22
+ def calculate_article_rank
23
+ @articles.each do |article|
24
+ title_words = article.title.split(/\W+/)
25
+ title_words.map! { |w| Keyword.first(word: w.downcase) }.compact
26
+
27
+ article.rank = 0
28
+ title_words.each do |kw|
29
+ article.rank += kw.rank unless kw.nil?
30
+ end
31
+
32
+ article.save
33
+ end
34
+ end
35
+
36
+ def suggest_articles
37
+ articles = Article.where(read: nil).exclude(rank: nil)
38
+ @article = articles.order(:rank).limit(1).reverse.first
39
+
40
+ @output.puts <<EOF
41
+ [#{@article.id}] #{@article.title} (#{@article.rank})
42
+ #{@article.url}
43
+
44
+ EOF
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,3 @@
1
+ module HNews
2
+ VERSION = "0.1.0"
3
+ end
data/test/factories.rb ADDED
@@ -0,0 +1,22 @@
1
+ require "hnews/models"
2
+ require "hnews/models/article"
3
+ require "helpers/sequel"
4
+
5
+ FactoryGirl.define do
6
+ factory :keyword do
7
+ rank 0
8
+ last_used Time.now
9
+ end
10
+
11
+ factory :article do
12
+ title "Programmers block"
13
+ url "http://gregchapple.com/programmers-block/"
14
+ content "Your fingers sit idly on your keyboard, eagerly awaiting the signal from your brain to produce this masterpiece of software. Nothing."
15
+ end
16
+
17
+ factory :article_with_content, class: Article do
18
+ title "Programmers block"
19
+ url "http://gregchapple.com/programmers-block/"
20
+ content "Your fingers sit idly on your keyboard, eagerly awaiting the signal from your brain to produce this masterpiece of software. Nothing."
21
+ end
22
+ end
@@ -0,0 +1,9 @@
1
+ class MiniTest::Spec
2
+ before :each do
3
+ DatabaseCleaner.start
4
+ end
5
+
6
+ after :each do
7
+ DatabaseCleaner.clean
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module Sequel
2
+ class Model
3
+ def save!
4
+ save
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ require "test_helper"
2
+ require "hnews/services/ingest"
3
+
4
+ describe HNews::Ingest do
5
+ before do
6
+ @ingest = HNews::Ingest.new
7
+ end
8
+
9
+ it "ingests articles from Hacker News" do
10
+ @ingest.start
11
+
12
+ @ingest.articles.length.must_equal 30
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ require "test_helper"
2
+ require "hnews/services/scrape_article"
3
+
4
+ describe HNews::ScrapeArticle do
5
+ before do
6
+ @article = create(:article)
7
+ @output = StringIO.new
8
+ end
9
+
10
+ it "sets the articles content attribute" do
11
+ service = HNews::ScrapeArticle.new @article, input: nil, output: @output
12
+ service.start
13
+
14
+ article = Article.first(id: @article.id)
15
+ article.content.wont_be_empty
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ ENV["APP_ENV"] = "test"
2
+
3
+ require "sequel"
4
+ require "factory_girl"
5
+ require "database_cleaner"
6
+ require "config"
7
+ require "factories"
8
+
9
+ require "minitest"
10
+ require "minitest/autorun"
11
+
12
+
13
+ DatabaseCleaner.strategy = :transaction
14
+ class MiniTest::Spec
15
+ before :each do
16
+ DatabaseCleaner.start
17
+ end
18
+
19
+ after :each do
20
+ DatabaseCleaner.clean
21
+ end
22
+ end
23
+
24
+ class MiniTest::Spec
25
+ include FactoryGirl::Syntax::Methods
26
+ end
@@ -0,0 +1,7 @@
1
+ require "test_helper"
2
+
3
+ describe "Bootstrap test" do
4
+ it "can be run" do
5
+ assert true
6
+ end
7
+ end
@@ -0,0 +1,66 @@
1
+ require "test_helper"
2
+ require "hnews/services/index_article"
3
+
4
+ describe HNews::IndexArticle do
5
+ before do
6
+ @input = StringIO.new
7
+ @output = StringIO.new
8
+ @article = create(:article_with_content)
9
+ end
10
+
11
+ it "outputs the correct message" do
12
+ service = HNews::IndexArticle.new(@article, {input: @input, output: @output})
13
+ service.start
14
+
15
+ @output.string.must_include "Indexed #{@article.title}"
16
+ end
17
+
18
+ it "filters out end words" do
19
+ service = HNews::IndexArticle.new(@article, {input: @input, output: @output})
20
+ service.start
21
+
22
+ content = @article.content
23
+ words = content.split(/\W+/)
24
+
25
+ HNews::END_WORDS.each do |end_word|
26
+ words.wont_include end_word
27
+ end
28
+ end
29
+
30
+ it "generates keywords" do
31
+ service = HNews::IndexArticle.new(@article, {input: @input, output: @output})
32
+ service.start
33
+
34
+ Keyword.all.count.must_be :>, 0
35
+ end
36
+
37
+ it "increases the keyword rank" do
38
+ keyword = create(:keyword, {word: 'keyboard', rank: 5})
39
+ service = HNews::IndexArticle.new(@article, {input: @input, output: @output})
40
+ service.start
41
+
42
+ keyword = Keyword.first(word: 'keyboard')
43
+
44
+ keyword.rank.must_equal 6
45
+ end
46
+
47
+ it "update the last_used attribute on the keywords" do
48
+ now = Time.now
49
+ keyword = create(:keyword, {word: 'keyboard', last_used: now})
50
+ service = HNews::IndexArticle.new(@article, {input: @input, output: @output})
51
+ service.start
52
+
53
+ keyword = Keyword.first(word: 'keyboard')
54
+
55
+ keyword.last_used.wont_equal now
56
+ end
57
+
58
+ it "indexes the articles title" do
59
+ article = create(:article, {title: "Ruby programming", content: "this is about ruby", rank: 0})
60
+ service = HNews::IndexArticle.new(article, {input: @input, output: @output})
61
+ service.start
62
+
63
+ word = Keyword.first(word: "programming")
64
+ word.wont_be_nil
65
+ end
66
+ end
@@ -0,0 +1,28 @@
1
+ require "test_helper"
2
+ require "hnews/services/list_articles"
3
+
4
+ describe HNews::ListArticles do
5
+ it "lists articles" do
6
+ article = create(:article, {title: "Programmers block", url: "http://gregchapple.com/programmers-block" })
7
+
8
+ output = StringIO.new
9
+ service = HNews::ListArticles.new input: nil, output: output
10
+ service.start
11
+
12
+ output.string.must_include "[#{article.id}] Programmers block"
13
+ end
14
+
15
+ it "lists thirty articles" do
16
+ 35.times do |i|
17
+ create(:article)
18
+ end
19
+
20
+ output = StringIO.new
21
+ service = HNews::ListArticles.new input: nil, output: output
22
+ service.start
23
+
24
+ articles = output.string.split("\n")
25
+
26
+ articles.length.must_equal 30
27
+ end
28
+ end
@@ -0,0 +1,54 @@
1
+ require "test_helper"
2
+ require "hnews/services/pick_article"
3
+
4
+ describe HNews::PickArticle do
5
+ before do
6
+ @article = create(:article)
7
+ @output = StringIO.new
8
+ end
9
+
10
+ it "allows an id to be entered" do
11
+ input = StringIO.new("#{@article.id}\n")
12
+
13
+ service = HNews::PickArticle.new input: input, output: @output
14
+ service.start
15
+
16
+ @output.string.must_include "Please enter an id:"
17
+ end
18
+
19
+ it "allows an article to be chosen" do
20
+ input = StringIO.new("#{@article.id}\n")
21
+
22
+ service = HNews::PickArticle.new input: input, output: @output
23
+ service.start
24
+
25
+ @output.string.must_include "Picked: Programmers block"
26
+ end
27
+
28
+ it "id can't be nil" do
29
+ input = StringIO.new("\n")
30
+
31
+ service = HNews::PickArticle.new input: input, output: @output
32
+ service.start
33
+
34
+ @output.string.must_include "Not a valid id"
35
+ end
36
+
37
+ it "fails if id is not found" do
38
+ input = StringIO.new("99999\n")
39
+
40
+ service = HNews::PickArticle.new input: input, output: @output
41
+ service.start
42
+
43
+ @output.string.must_include "Not a valid id"
44
+ end
45
+
46
+ it "allows the articled to be accessed" do
47
+ input = StringIO.new("#{@article.id}\n")
48
+
49
+ service = HNews::PickArticle.new input: input, output: @output
50
+ service.start
51
+
52
+ service.must_respond_to :article
53
+ end
54
+ end
@@ -0,0 +1,57 @@
1
+ require "test_helper"
2
+ require "hnews/services/suggest_article"
3
+
4
+ describe HNews::IndexArticle do
5
+ it "suggests an article" do
6
+ create(:keyword, {word: 'software', rank: 5})
7
+ create(:keyword, {word: 'keyboard', rank: 2})
8
+ create(:keyword, {word: 'development', rank: 6})
9
+
10
+ create(:article_with_content)
11
+ article = create(:article, {title: 'On Software Development', rank: 0})
12
+
13
+ output = StringIO.new
14
+ service = HNews::SuggestArticle.new input: nil, output: output
15
+ service.start
16
+
17
+ output.string.must_include "[#{article.id}] On Software Development"
18
+ output.string.must_include article.url
19
+ end
20
+
21
+ it "sets the articles rank" do
22
+ create(:keyword, {word: 'software', rank: 5})
23
+ create(:keyword, {word: 'keyboard', rank: 2})
24
+ create(:keyword, {word: 'development', rank: 6})
25
+
26
+ create(:article_with_content)
27
+ article = create(:article, {title: 'On Software Development', rank: 0})
28
+
29
+ output = StringIO.new
30
+ service = HNews::SuggestArticle.new input: nil, output: output
31
+ service.start
32
+
33
+ article = Article.first(id: article.id)
34
+
35
+ article.rank.must_be :>, 0
36
+ end
37
+
38
+ it "doesn't constantly increase articles rank" do
39
+ create(:keyword, {word: 'software', rank: 5})
40
+ create(:keyword, {word: 'keyboard', rank: 2})
41
+ create(:keyword, {word: 'development', rank: 6})
42
+
43
+ create(:article_with_content)
44
+ article = create(:article, {title: 'On Software Development', rank: 0})
45
+
46
+ output = StringIO.new
47
+ service = HNews::SuggestArticle.new input: nil, output: output
48
+
49
+ # run the service twice
50
+ service.start
51
+ service.start
52
+
53
+ article = Article.first(id: article.id)
54
+
55
+ article.rank.must_equal 11
56
+ end
57
+ end
metadata ADDED
@@ -0,0 +1,232 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hnews-suggest
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Greg Chapple
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: factory_girl
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: database_cleaner
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 4.1.1
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 4.1.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: sequel
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sqlite3
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: nokogiri
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: thor
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: launchy
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: Suggest articles from Hacker News. Suggestions will get better over time
154
+ as the gem learns what you like!
155
+ email:
156
+ - gregchapple1@gmail.com
157
+ executables:
158
+ - hnews
159
+ extensions: []
160
+ extra_rdoc_files: []
161
+ files:
162
+ - ".gitignore"
163
+ - Gemfile
164
+ - LICENSE
165
+ - README.md
166
+ - Rakefile
167
+ - bin/hnews
168
+ - config/config.rb
169
+ - db/migrate/001_create_articles.rb
170
+ - db/migrate/002_create_keywords.rb
171
+ - db/migrate/003_add_content_to_articles.rb
172
+ - hnews.gemspec
173
+ - lib/hnews.rb
174
+ - lib/hnews/cli.rb
175
+ - lib/hnews/models.rb
176
+ - lib/hnews/models/article.rb
177
+ - lib/hnews/models/keyword.rb
178
+ - lib/hnews/services.rb
179
+ - lib/hnews/services/index_article.rb
180
+ - lib/hnews/services/ingest.rb
181
+ - lib/hnews/services/list_articles.rb
182
+ - lib/hnews/services/pick_article.rb
183
+ - lib/hnews/services/scrape_article.rb
184
+ - lib/hnews/services/suggest_article.rb
185
+ - lib/hnews/version.rb
186
+ - test/factories.rb
187
+ - test/helpers/database_cleaner.rb
188
+ - test/helpers/sequel.rb
189
+ - test/integration/article_ingest_test.rb
190
+ - test/integration/scrape_article_test.rb
191
+ - test/test_helper.rb
192
+ - test/unit/bootstrap_test.rb
193
+ - test/unit/index_article_test.rb
194
+ - test/unit/list_article_test.rb
195
+ - test/unit/pick_article_test.rb
196
+ - test/unit/suggest_article_test.rb
197
+ homepage: ''
198
+ licenses:
199
+ - MIT
200
+ metadata: {}
201
+ post_install_message:
202
+ rdoc_options: []
203
+ require_paths:
204
+ - lib
205
+ required_ruby_version: !ruby/object:Gem::Requirement
206
+ requirements:
207
+ - - ">="
208
+ - !ruby/object:Gem::Version
209
+ version: 1.9.3
210
+ required_rubygems_version: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0'
215
+ requirements: []
216
+ rubyforge_project:
217
+ rubygems_version: 2.2.2
218
+ signing_key:
219
+ specification_version: 4
220
+ summary: Automatically suggest articles from Hacker News
221
+ test_files:
222
+ - test/factories.rb
223
+ - test/helpers/database_cleaner.rb
224
+ - test/helpers/sequel.rb
225
+ - test/integration/article_ingest_test.rb
226
+ - test/integration/scrape_article_test.rb
227
+ - test/test_helper.rb
228
+ - test/unit/bootstrap_test.rb
229
+ - test/unit/index_article_test.rb
230
+ - test/unit/list_article_test.rb
231
+ - test/unit/pick_article_test.rb
232
+ - test/unit/suggest_article_test.rb