hnews-suggest 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +45 -0
- data/Rakefile +23 -0
- data/bin/hnews +33 -0
- data/config/config.rb +7 -0
- data/db/migrate/001_create_articles.rb +11 -0
- data/db/migrate/002_create_keywords.rb +10 -0
- data/db/migrate/003_add_content_to_articles.rb +5 -0
- data/hnews.gemspec +35 -0
- data/lib/hnews.rb +3 -0
- data/lib/hnews/cli.rb +59 -0
- data/lib/hnews/models.rb +2 -0
- data/lib/hnews/models/article.rb +2 -0
- data/lib/hnews/models/keyword.rb +2 -0
- data/lib/hnews/services.rb +6 -0
- data/lib/hnews/services/index_article.rb +55 -0
- data/lib/hnews/services/ingest.rb +48 -0
- data/lib/hnews/services/list_articles.rb +25 -0
- data/lib/hnews/services/pick_article.rb +33 -0
- data/lib/hnews/services/scrape_article.rb +28 -0
- data/lib/hnews/services/suggest_article.rb +48 -0
- data/lib/hnews/version.rb +3 -0
- data/test/factories.rb +22 -0
- data/test/helpers/database_cleaner.rb +9 -0
- data/test/helpers/sequel.rb +7 -0
- data/test/integration/article_ingest_test.rb +14 -0
- data/test/integration/scrape_article_test.rb +17 -0
- data/test/test_helper.rb +26 -0
- data/test/unit/bootstrap_test.rb +7 -0
- data/test/unit/index_article_test.rb +66 -0
- data/test/unit/list_article_test.rb +28 -0
- data/test/unit/pick_article_test.rb +54 -0
- data/test/unit/suggest_article_test.rb +57 -0
- metadata +232 -0
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
data/Gemfile
ADDED
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
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
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
|
data/lib/hnews/models.rb
ADDED
@@ -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
|
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,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
|
data/test/test_helper.rb
ADDED
@@ -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,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
|