harrisj-nytimes-articles 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION.yml +4 -0
- data/lib/nytimes_articles.rb +8 -0
- data/lib/nytimes_articles/article.rb +397 -0
- data/lib/nytimes_articles/base.rb +110 -0
- data/lib/nytimes_articles/exceptions.rb +24 -0
- data/lib/nytimes_articles/facet.rb +70 -0
- data/lib/nytimes_articles/result_set.rb +40 -0
- data/test/nytimes/articles/test_article.rb +527 -0
- data/test/nytimes/articles/test_base.rb +92 -0
- data/test/nytimes/articles/test_facet.rb +46 -0
- data/test/nytimes/articles/test_result_set.rb +62 -0
- data/test/test_helper.rb +31 -0
- metadata +78 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
module Nytimes
|
2
|
+
module Articles
|
3
|
+
class Error < ::RuntimeError
|
4
|
+
end
|
5
|
+
|
6
|
+
class AuthenticationError < Error
|
7
|
+
end
|
8
|
+
|
9
|
+
class BadRequestError < Error
|
10
|
+
end
|
11
|
+
|
12
|
+
class BadResponseError < Error
|
13
|
+
end
|
14
|
+
|
15
|
+
class ServerError < Error
|
16
|
+
end
|
17
|
+
|
18
|
+
class TimeoutError < Error
|
19
|
+
end
|
20
|
+
|
21
|
+
class ConnectionError < Error
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Nytimes
|
2
|
+
module Articles
|
3
|
+
class Facet
|
4
|
+
attr_reader :term, :count, :facet_type
|
5
|
+
|
6
|
+
# Facet name constants
|
7
|
+
CLASSIFIERS = 'classifiers_facet'
|
8
|
+
COLUMN = 'column_facet'
|
9
|
+
DATE = 'date'
|
10
|
+
DAY_OF_WEEK = 'day_of_week_facet'
|
11
|
+
DESCRIPTION = 'des_facet'
|
12
|
+
DESK = 'desk_facet'
|
13
|
+
GEO = 'geo_facet'
|
14
|
+
MATERIAL_TYPE = 'material_type_facet'
|
15
|
+
ORGANIZATION = 'org_facet'
|
16
|
+
PAGE = 'page_facet'
|
17
|
+
PERSON = 'per_facet'
|
18
|
+
PUB_DAY = 'publication_day'
|
19
|
+
PUB_MONTH = 'publication_month'
|
20
|
+
PUB_YEAR = 'publication_year'
|
21
|
+
SECTION_PAGE = 'section_page_facet'
|
22
|
+
SOURCE = 'source_facet'
|
23
|
+
WORKS_MENTIONED = 'works_mentioned_facet'
|
24
|
+
|
25
|
+
# Facets of content formatted for nytimes.com
|
26
|
+
NYTD_BYLINE = 'nytd_byline'
|
27
|
+
NYTD_DESCRIPTION = 'nytd_des_facet'
|
28
|
+
NYTD_GEO = 'nytd_geo_facet'
|
29
|
+
NYTD_ORGANIZATION = 'nytd_org_facet'
|
30
|
+
NYTD_PERSON = 'nytd_per_facet'
|
31
|
+
NYTD_SECTION = 'nytd_section_facet'
|
32
|
+
NYTD_WORKS_MENTIONED = 'nytd_works_mentioned_facet'
|
33
|
+
|
34
|
+
# The best 5 facets to return
|
35
|
+
DEFAULT_RETURN_FACETS = [NYTD_DESCRIPTION, NYTD_GEO, NYTD_ORGANIZATION, NYTD_PERSON, NYTD_SECTION]
|
36
|
+
|
37
|
+
ALL_FACETS = [CLASSIFIERS, COLUMN, DATE, DAY_OF_WEEK, DESCRIPTION, DESK, GEO, MATERIAL_TYPE, ORGANIZATION, PAGE, PERSON, PUB_DAY,
|
38
|
+
PUB_MONTH, PUB_YEAR, SECTION_PAGE, SOURCE, WORKS_MENTIONED, NYTD_BYLINE, NYTD_DESCRIPTION, NYTD_GEO,
|
39
|
+
NYTD_ORGANIZATION, NYTD_PERSON, NYTD_SECTION, NYTD_WORKS_MENTIONED]
|
40
|
+
|
41
|
+
def initialize(facet_type, term, count)
|
42
|
+
@facet_type = facet_type
|
43
|
+
@term = term
|
44
|
+
@count = count
|
45
|
+
end
|
46
|
+
|
47
|
+
# def self.init_from_api(type, hash)
|
48
|
+
# self.new(type, hash['term'], hash['count'].to_i)
|
49
|
+
# end
|
50
|
+
|
51
|
+
def self.init_from_api(api_hash)
|
52
|
+
return nil if api_hash.nil?
|
53
|
+
|
54
|
+
unless api_hash.is_a? Hash
|
55
|
+
raise ArgumentError, "expecting a Hash only"
|
56
|
+
else
|
57
|
+
return nil if api_hash.empty?
|
58
|
+
end
|
59
|
+
|
60
|
+
out = {}
|
61
|
+
|
62
|
+
api_hash.each_pair do |k,v|
|
63
|
+
out[k] = v.map {|f| Facet.new(k, f['term'], f['count'])}
|
64
|
+
end
|
65
|
+
|
66
|
+
out
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Nytimes
|
5
|
+
module Articles
|
6
|
+
class ResultSet < Base
|
7
|
+
extend Forwardable
|
8
|
+
attr_reader :offset, :total_results, :results, :facets
|
9
|
+
|
10
|
+
BATCH_SIZE = 10
|
11
|
+
|
12
|
+
def_delegators :@results, :&, :*, :+, :-, :[], :at, :collect, :compact, :each, :each_index, :empty?, :fetch, :first, :include?, :index, :last, :length, :map, :nitems, :reject, :reverse, :reverse_each, :rindex, :select, :size, :slice
|
13
|
+
|
14
|
+
def initialize(params)
|
15
|
+
@offset = params[:offset]
|
16
|
+
@total_results = params[:total_results]
|
17
|
+
@results = params[:results]
|
18
|
+
@facets = params[:facets]
|
19
|
+
end
|
20
|
+
|
21
|
+
def page_number
|
22
|
+
return 0 if @total_results == 0
|
23
|
+
@offset + 1
|
24
|
+
end
|
25
|
+
|
26
|
+
def total_pages
|
27
|
+
return 0 if @total_results == 0
|
28
|
+
(@total_results.to_f / BATCH_SIZE).ceil
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.init_from_api(api_hash)
|
32
|
+
self.new(:offset => integer_field(api_hash['offset']),
|
33
|
+
:total_results => integer_field(api_hash['total']),
|
34
|
+
:results => api_hash['results'].map {|r| Article.init_from_api(r)},
|
35
|
+
:facets => Facet.init_from_api(api_hash['facets'])
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,527 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../../test_helper.rb'
|
2
|
+
|
3
|
+
ARTICLE_API_HASH = {"page_facet"=>"8", "lead_paragraph"=>"", "classifiers_facet"=>["Top/News/Business", "Top/Classifieds/Job Market/Job Categories/Banking, Finance and Insurance", "Top/News/Business/Markets"], "title"=>"Wall St. Treads Water as It Waits on Washington", "nytd_title"=>"Wall St. Treads Water as It Waits on Washington", "byline"=>"By JACK HEALY", "body"=>"Wall Street held its breath on Monday as it awaited details on a banking bailout from Washington. Investors had expected to start the week with an announcement from the Treasury Department outlining its latest plans to stabilize the financial system. But the Obama administration delayed releasing the details until at least Tuesday to keep the focus", "material_type_facet"=>["News"], "url"=>"http://www.nytimes.com/2009/02/10/business/10markets.html", "publication_month"=>"02", "date"=>"20090210", "publication_year"=>"2009", "nytd_section_facet"=>["Business"], "source_facet"=>"The New York Times", "desk_facet"=>"Business", "publication_day"=>"10", "des_facet"=>["STOCKS AND BONDS"], "day_of_week_facet"=>"Tuesday"}
|
4
|
+
|
5
|
+
ARTICLE_API_HASH2 = {"page_facet"=>"29", "lead_paragraph"=>"", "geo_facet"=>["WALL STREET (NYC)"], "small_image_width"=>"75", "classifiers_facet"=>["Top/News/New York and Region", "Top/Classifieds/Job Market/Job Categories/Education", "Top/Features/Travel/Guides/Destinations/North America", "Top/Classifieds/Job Market/Job Categories/Banking, Finance and Insurance", "Top/Features/Travel/Guides/Destinations/North America/United States/New York", "Top/Features/Travel/Guides/Destinations/North America/United States", "Top/News/Education"], "title"=>"OUR TOWNS; As Pipeline to Wall Street Narrows, Princeton Students Adjust Sights", "nytd_title"=>"As Pipeline to Wall Street Narrows, Princeton Students Adjust Sights", "byline"=>"By PETER APPLEBOME", "body"=>"Princeton, N.J. There must be a screenplay in the fabulous Schoppe twins, Christine and Jennifer, Princeton University juniors from Houston. They had the same G.P.A. and SATs in high school, where they became Gold Award Girl Scouts , sort of the female version of Eagle Scouts. They live together and take all the same courses, wear identical necklac", "material_type_facet"=>["News"], "url"=>"http://www.nytimes.com/2009/02/08/nyregion/08towns.html", "publication_month"=>"02", "small_image_height"=>"75", "date"=>"20090208", "column_facet"=>"Our Towns", "small_image"=>"Y", "publication_year"=>"2009", "nytd_section_facet"=>["New York and Region", "Education"], "source_facet"=>"The New York Times", "org_facet"=>["PRINCETON UNIVERSITY"], "desk_facet"=>"New York Region", "publication_day"=>"08", "small_image_url"=>"http://graphics8.nytimes.com/images/2009/02/08/nyregion/08towns.751.jpg", "des_facet"=>["EDUCATION AND SCHOOLS", "BANKS AND BANKING"], "day_of_week_facet"=>"Sunday"}
|
6
|
+
|
7
|
+
class TestNytimes::TestArticles::TestArticle < Test::Unit::TestCase
|
8
|
+
include Nytimes::Articles
|
9
|
+
|
10
|
+
def setup
|
11
|
+
init_test_key
|
12
|
+
Article.stubs(:parse_reply)
|
13
|
+
end
|
14
|
+
|
15
|
+
context "Article.search" do
|
16
|
+
should "accept a String for the first argument that is passed through to the query in the API" do
|
17
|
+
Article.expects(:invoke).with(has_entry("query", "FOO BAR"))
|
18
|
+
Article.search "FOO BAR"
|
19
|
+
end
|
20
|
+
|
21
|
+
should "accept a Hash for the first argument" do
|
22
|
+
Article.expects(:invoke).with(has_entry("query", "FOO BAR"))
|
23
|
+
Article.search :query => 'FOO BAR', :page => 2
|
24
|
+
end
|
25
|
+
|
26
|
+
context "date ranges" do
|
27
|
+
should "pass a string argument to begin_date straight through" do
|
28
|
+
date = "20081212"
|
29
|
+
Article.expects(:invoke).with(has_entry("begin_date", date))
|
30
|
+
Article.search :begin_date => date
|
31
|
+
end
|
32
|
+
|
33
|
+
should "convert begin_date from a Date or Time to YYYYMMDD format" do
|
34
|
+
time = Time.now
|
35
|
+
Article.expects(:invoke).with(has_entry("begin_date", time.strftime("%Y%m%d")))
|
36
|
+
Article.search :begin_date => time
|
37
|
+
end
|
38
|
+
|
39
|
+
should "pass a string argument to end_date straight through" do
|
40
|
+
date = "20081212"
|
41
|
+
Article.expects(:invoke).with(has_entry("end_date", date))
|
42
|
+
Article.search :end_date => date
|
43
|
+
end
|
44
|
+
|
45
|
+
should "convert end_date from a Date or Time to YYYYMMDD format" do
|
46
|
+
time = Time.now
|
47
|
+
Article.expects(:invoke).with(has_entry("end_date", time.strftime("%Y%m%d")))
|
48
|
+
Article.search :end_date => time
|
49
|
+
end
|
50
|
+
|
51
|
+
should "raise an ArgumentError if the begin_date is NOT a string and does not respond_to strftime" do
|
52
|
+
assert_raise(ArgumentError) { Article.search :begin_date => 23 }
|
53
|
+
end
|
54
|
+
|
55
|
+
should "raise an ArgumentError if the end_date is NOT a string and does not respond_to strftime" do
|
56
|
+
assert_raise(ArgumentError) { Article.search :end_date => 23 }
|
57
|
+
end
|
58
|
+
|
59
|
+
context ":before" do
|
60
|
+
should "add a begin_date in 1980 if no :since or :begin_date argument is provided"
|
61
|
+
should "not add a begin_date is there is a :since argument"
|
62
|
+
should "not add a begin_date if there is a :begin_date argument already"
|
63
|
+
end
|
64
|
+
|
65
|
+
context ":since" do
|
66
|
+
should "add an end_date of now if no :before or :end_date argument is provided"
|
67
|
+
should "not add an end_date is there is a :before argument"
|
68
|
+
should "not add an end_date if there is a :end_date argument already"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "facets" do
|
73
|
+
should "accept a single string" do
|
74
|
+
Article.expects(:invoke).with(has_entry("facets", Facet::DATE))
|
75
|
+
Article.search "FOO BAR", :facets => Facet::DATE
|
76
|
+
end
|
77
|
+
|
78
|
+
should "accept an array of strings" do
|
79
|
+
Article.expects(:invoke).with(has_entry("facets", [Facet::DATE, Facet::GEO].join(',')))
|
80
|
+
Article.search "FOO BAR", :facets => [Facet::DATE, Facet::GEO]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context "search_facets" do
|
85
|
+
should "accept a String" do
|
86
|
+
Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA]"))
|
87
|
+
Article.search :search_facets => "#{Facet::GEO}:[CALIFORNIA]"
|
88
|
+
end
|
89
|
+
|
90
|
+
should "accept a single hash value Facet string to a term" do
|
91
|
+
Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA]"))
|
92
|
+
Article.search :search_facets => {Facet::GEO => 'CALIFORNIA'}
|
93
|
+
end
|
94
|
+
|
95
|
+
should "accept an Facet string hashed to an array terms" do
|
96
|
+
Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA,GREAT BRITAIN]"))
|
97
|
+
Article.search :search_facets => {Facet::GEO => ['CALIFORNIA', 'GREAT BRITAIN']}
|
98
|
+
end
|
99
|
+
|
100
|
+
should "accept a single Facet object" do
|
101
|
+
f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
|
102
|
+
Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA]"))
|
103
|
+
Article.search :search_facets => f
|
104
|
+
end
|
105
|
+
|
106
|
+
should "accept an array of Facet objects" do
|
107
|
+
f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
|
108
|
+
f2 = Facet.new(Facet::NYTD_ORGANIZATION, 'University Of California', 12)
|
109
|
+
|
110
|
+
Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA] #{Facet::NYTD_ORGANIZATION}:[University Of California]"))
|
111
|
+
Article.search :search_facets => [f, f2]
|
112
|
+
end
|
113
|
+
|
114
|
+
should "merge multiple Facets objects in the array of the same type into one array" do
|
115
|
+
f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
|
116
|
+
f2 = Facet.new(Facet::GEO, 'IOWA', 12)
|
117
|
+
|
118
|
+
Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA,IOWA]"))
|
119
|
+
Article.search :search_facets => [f, f2]
|
120
|
+
end
|
121
|
+
|
122
|
+
should "not stomp on an existing query string" do
|
123
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream #{Facet::GEO}:[CALIFORNIA]"))
|
124
|
+
Article.search "ice cream", :search_facets => {Facet::GEO => "CALIFORNIA"}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context "exclude_facets" do
|
129
|
+
should "accept a String" do
|
130
|
+
Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA]"))
|
131
|
+
Article.search :exclude_facets => "-#{Facet::GEO}:[CALIFORNIA]"
|
132
|
+
end
|
133
|
+
|
134
|
+
should "accept a single hash value Facet string to a term" do
|
135
|
+
Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA]"))
|
136
|
+
Article.search :exclude_facets => {Facet::GEO => 'CALIFORNIA'}
|
137
|
+
end
|
138
|
+
|
139
|
+
should "accept an Facet string hashed to an array terms" do
|
140
|
+
Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA,GREAT BRITAIN]"))
|
141
|
+
Article.search :exclude_facets => {Facet::GEO => ['CALIFORNIA', 'GREAT BRITAIN']}
|
142
|
+
end
|
143
|
+
|
144
|
+
should "accept a single Facet object" do
|
145
|
+
f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
|
146
|
+
Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA]"))
|
147
|
+
Article.search :exclude_facets => f
|
148
|
+
end
|
149
|
+
|
150
|
+
should "accept an array of Facet objects" do
|
151
|
+
f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
|
152
|
+
f2 = Facet.new(Facet::NYTD_ORGANIZATION, 'University Of California', 12)
|
153
|
+
|
154
|
+
Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA] -#{Facet::NYTD_ORGANIZATION}:[University Of California]"))
|
155
|
+
Article.search :exclude_facets => [f, f2]
|
156
|
+
end
|
157
|
+
|
158
|
+
should "merge multiple Facets objects in the array of the same type into one array" do
|
159
|
+
f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
|
160
|
+
f2 = Facet.new(Facet::GEO, 'IOWA', 12)
|
161
|
+
|
162
|
+
Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA,IOWA]"))
|
163
|
+
Article.search :exclude_facets => [f, f2]
|
164
|
+
end
|
165
|
+
|
166
|
+
should "not stomp on an existing query string" do
|
167
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream -#{Facet::GEO}:[CALIFORNIA]"))
|
168
|
+
Article.search "ice cream", :exclude_facets => {Facet::GEO => "CALIFORNIA"}
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context ":fee" do
|
173
|
+
should "send through as fee:Y if set to true" do
|
174
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream fee:Y"))
|
175
|
+
Article.search "ice cream", :fee => true
|
176
|
+
end
|
177
|
+
|
178
|
+
should "send through as -fee:Y if set to false" do
|
179
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream -fee:Y"))
|
180
|
+
Article.search "ice cream", :fee => false
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
context ":fields" do
|
185
|
+
context "for the :all argument" do
|
186
|
+
should "pass all fields in a comma-delimited list" do
|
187
|
+
Article.expects(:invoke).with(has_entry('fields', Article::ALL_FIELDS.join(',')))
|
188
|
+
Article.search "FOO BAR", :fields => :all
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
context "for the :none argument" do
|
193
|
+
should "request a blank space for the fields argument"
|
194
|
+
should "request the standard :facets if no :facets have been explicitly provided"
|
195
|
+
should "request the given :facets field if provided"
|
196
|
+
end
|
197
|
+
|
198
|
+
context ":thumbnail" do
|
199
|
+
should "accept the symbol version of the argument"
|
200
|
+
should "accept the string version of the argument"
|
201
|
+
should "request all the thumbnail image fields from the API"
|
202
|
+
end
|
203
|
+
|
204
|
+
context ":multimedia" do
|
205
|
+
should "be implemented"
|
206
|
+
end
|
207
|
+
|
208
|
+
should "accept a single string as an argument" do
|
209
|
+
Article.expects(:invoke).with(has_entry('fields', 'body'))
|
210
|
+
Article.search "FOO BAR", :fields => 'body'
|
211
|
+
end
|
212
|
+
|
213
|
+
should "accept a single symbol as an argument" do
|
214
|
+
Article.expects(:invoke).with(has_entry('fields', 'body'))
|
215
|
+
Article.search "FOO BAR", :fields => :body
|
216
|
+
end
|
217
|
+
|
218
|
+
should "accept an array of strings and symbols" do
|
219
|
+
Article.expects(:invoke).with(has_entry('fields', 'abstract,body'))
|
220
|
+
Article.search "FOO BAR", :fields => [:abstract, 'body']
|
221
|
+
end
|
222
|
+
|
223
|
+
should "raise an ArgumentError otherwise" do
|
224
|
+
assert_raise(ArgumentError) { Article.search :fields => 12 }
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
context ":has_multimedia" do
|
229
|
+
should "send through as related_multimedia:Y if set to true" do
|
230
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream related_multimedia:Y"))
|
231
|
+
Article.search "ice cream", :has_multimedia => true
|
232
|
+
end
|
233
|
+
|
234
|
+
should "send through as -related_multimedia:Y if set to false" do
|
235
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream -related_multimedia:Y"))
|
236
|
+
Article.search "ice cream", :has_multimedia => false
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
context ":has_thumbnail" do
|
241
|
+
should "send through as small_image:Y if set to true" do
|
242
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream small_image:Y"))
|
243
|
+
Article.search "ice cream", :has_thumbnail => true
|
244
|
+
end
|
245
|
+
|
246
|
+
should "send through as -small_image:Y if set to false" do
|
247
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream -small_image:Y"))
|
248
|
+
Article.search "ice cream", :has_thumbnail => false
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
context ":offset" do
|
253
|
+
should "pass through an explicit offset parameter if specified" do
|
254
|
+
Article.expects(:invoke).with(has_entry("offset", 10))
|
255
|
+
Article.search :offset => 10
|
256
|
+
end
|
257
|
+
|
258
|
+
should "raise an ArgumentError if the offset is not an Integer" do
|
259
|
+
assert_raise(ArgumentError) { Article.search :offset => 'apple' }
|
260
|
+
end
|
261
|
+
|
262
|
+
should "pass through an offset of page - 1 if :page is used instead" do
|
263
|
+
Article.expects(:invoke).with(has_entry("offset", 2))
|
264
|
+
Article.search :page => 3
|
265
|
+
end
|
266
|
+
|
267
|
+
should "not pass through a page parameter to the API" do
|
268
|
+
Article.expects(:invoke).with(Not(has_key("page")))
|
269
|
+
Article.search :page => 3
|
270
|
+
end
|
271
|
+
|
272
|
+
should "raise an ArgumentError if the page is not an Integer" do
|
273
|
+
assert_raise(ArgumentError) { Article.search :page => 'orange' }
|
274
|
+
end
|
275
|
+
|
276
|
+
should "raise an ArgumentError if the page is less than 1" do
|
277
|
+
assert_raise(ArgumentError) { Article.search :page => 0 }
|
278
|
+
end
|
279
|
+
|
280
|
+
should "use the :offset argument if both an :offset and :page are provided" do
|
281
|
+
Article.expects(:invoke).with(has_entry("offset", 2))
|
282
|
+
Article.search :offset => 2, :page => 203
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
context "rank" do
|
287
|
+
%w(newest oldest closest).each do |rank|
|
288
|
+
should "accept #{rank} as the argument to rank" do
|
289
|
+
Article.expects(:invoke).with(has_entry("rank", rank))
|
290
|
+
Article.search :rank => rank.to_sym
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
should "raise an ArgumentError if rank is something else" do
|
295
|
+
assert_raise(ArgumentError) { Article.search :rank => :clockwise }
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
Article::TEXT_FIELDS.each do |tf|
|
300
|
+
context ":#{tf} parameter" do
|
301
|
+
should "prefix each non-quoted term with the #{tf}: field identifier in the query to the API" do
|
302
|
+
Article.expects(:invoke).with(has_entry("query", "#{tf}:ice #{tf}:cream"))
|
303
|
+
Article.search tf.to_sym => 'ice cream'
|
304
|
+
end
|
305
|
+
|
306
|
+
should "prefix -terms (excluded terms) with -#{tf}:" do
|
307
|
+
Article.expects(:invoke).with(has_entry("query", "#{tf}:ice -#{tf}:cream"))
|
308
|
+
Article.search tf.to_sym => 'ice -cream'
|
309
|
+
end
|
310
|
+
|
311
|
+
should "put quoted terms behind the field spec" do
|
312
|
+
Article.expects(:invoke).with(has_entry("query", "#{tf}:\"ice cream\" #{tf}:cone"))
|
313
|
+
Article.search tf.to_sym => '"ice cream" cone'
|
314
|
+
end
|
315
|
+
|
316
|
+
should "handle complicated combinations of expressions" do
|
317
|
+
Article.expects(:invoke).with(has_entry("query", "#{tf}:\"ice cream\" -#{tf}:cone #{tf}:\"waffle\""))
|
318
|
+
Article.search tf.to_sym => '"ice cream" -cone "waffle"'
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# context "query parameters" do
|
324
|
+
# context "abstract" do
|
325
|
+
# should "be prefixed with the abstract: field identifier in the query"
|
326
|
+
# should "cast the argument to a string (will figure out processing later)"
|
327
|
+
# end
|
328
|
+
#
|
329
|
+
# context "author" do
|
330
|
+
# should "be prefixed with the author: field identifier in the query"
|
331
|
+
# should "cast the argument to a string (will figure out processing later)"
|
332
|
+
# end
|
333
|
+
#
|
334
|
+
# context "body" do
|
335
|
+
# should "be prefixed with the body: field identifier in the query"
|
336
|
+
# should "cast the argument to a string (will figure out processing later)"
|
337
|
+
# end
|
338
|
+
#
|
339
|
+
# context "byline" do
|
340
|
+
# should "be prefixed with the body: field identifier in the query"
|
341
|
+
# should "cast the argument to a string (will figure out processing later)"
|
342
|
+
# end
|
343
|
+
# end
|
344
|
+
end
|
345
|
+
|
346
|
+
context "Article.init_from_api" do
|
347
|
+
setup do
|
348
|
+
@article = Article.init_from_api(ARTICLE_API_HASH2)
|
349
|
+
end
|
350
|
+
|
351
|
+
Article::TEXT_FIELDS.each do |tf|
|
352
|
+
context "@#{tf}" do
|
353
|
+
should "read the value from the hash input" do
|
354
|
+
hash = {}
|
355
|
+
hash[tf] = "TEST TEXT"
|
356
|
+
article = Article.init_from_api(hash)
|
357
|
+
assert_equal "TEST TEXT", article.send(tf)
|
358
|
+
end
|
359
|
+
|
360
|
+
should "properly translate HTML entities back into characters" do
|
361
|
+
article = Article.init_from_api(tf => '“Money for Nothing”')
|
362
|
+
assert_equal "“Money for Nothing”", article.send(tf), article.inspect
|
363
|
+
end
|
364
|
+
|
365
|
+
should "only provide read-only access to the field" do
|
366
|
+
article = Article.init_from_api(tf => "TEST TEXT")
|
367
|
+
assert !article.respond_to?("#{tf}=")
|
368
|
+
end
|
369
|
+
|
370
|
+
should "return nil if the value is not provided in the hash" do
|
371
|
+
article = Article.init_from_api({"foo" => "bar"})
|
372
|
+
assert_nil article.send(tf)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
Article::NUMERIC_FIELDS.each do |tf|
|
378
|
+
context "@#{tf}" do
|
379
|
+
should "read and coerce the string value from the hash input" do
|
380
|
+
article = Article.init_from_api(tf => "23")
|
381
|
+
assert_equal 23, article.send(tf)
|
382
|
+
end
|
383
|
+
|
384
|
+
should "only provide read-only access to the field" do
|
385
|
+
article = Article.init_from_api(tf => "23")
|
386
|
+
assert !article.respond_to?("#{tf}=")
|
387
|
+
end
|
388
|
+
|
389
|
+
should "return nil if the value is not provided in the hash" do
|
390
|
+
article = Article.init_from_api({"foo" => "bar"})
|
391
|
+
assert_nil article.send(tf)
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
# all the rest
|
397
|
+
context "@fee" do
|
398
|
+
setup do
|
399
|
+
@article = Article.init_from_api(ARTICLE_API_HASH)
|
400
|
+
end
|
401
|
+
|
402
|
+
should "be true if returned as true from the API" do
|
403
|
+
article = Article.init_from_api('fee' => true)
|
404
|
+
assert_equal true, article.fee?
|
405
|
+
assert_equal false, article.free?
|
406
|
+
end
|
407
|
+
|
408
|
+
should "default to false if not specified in the hash" do
|
409
|
+
assert_equal false, @article.fee?
|
410
|
+
assert_equal true, @article.free?
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
context "@url" do
|
415
|
+
setup do
|
416
|
+
@article = Article.init_from_api(ARTICLE_API_HASH)
|
417
|
+
end
|
418
|
+
|
419
|
+
should "read the value from the hash" do
|
420
|
+
assert_equal ARTICLE_API_HASH['url'], @article.url
|
421
|
+
end
|
422
|
+
|
423
|
+
should "return a String" do
|
424
|
+
assert_kind_of(String, @article.url)
|
425
|
+
end
|
426
|
+
|
427
|
+
should "only provide read-only access to the field" do
|
428
|
+
assert !@article.respond_to?("url=")
|
429
|
+
end
|
430
|
+
|
431
|
+
should "return nil if the value is not provided in the hash" do
|
432
|
+
article = Article.init_from_api({"foo" => "bar"})
|
433
|
+
assert_nil article.url
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
context "@page" do
|
438
|
+
should "read the value from the page_facet field" do
|
439
|
+
assert_equal ARTICLE_API_HASH2['page_facet'].to_i, @article.page
|
440
|
+
end
|
441
|
+
|
442
|
+
should "only provide read-only access to the field" do
|
443
|
+
article = Article.new
|
444
|
+
assert !article.respond_to?("page=")
|
445
|
+
end
|
446
|
+
|
447
|
+
should "return nil if the value is not provided in the hash" do
|
448
|
+
article = Article.init_from_api({"foo" => "bar"})
|
449
|
+
assert_nil article.page
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
|
456
|
+
# abstract String X X A summary of the article, written by Times indexers
|
457
|
+
# author String X X An author note, such as an e-mail address or short biography (compare byline)
|
458
|
+
# body String X X A portion of the beginning of the article. Note: Only a portion of the article body is included in responses. But when you search against the body field, you search the full text of the article.
|
459
|
+
# byline String X X The article byline, including the author's name
|
460
|
+
# classifers_facet Array (Strings) X X Taxonomic classifiers that reflect Times content categories, such as Top/News/Sports
|
461
|
+
# column_facet String X X A Times column title (if applicable), such as Weddings or Ideas & Trends
|
462
|
+
# date Date X X The publication date in YYYYMMDD format
|
463
|
+
# day_of_week_facet String X X The day of the week (e.g., Monday, Tuesday) the article was published (compare publication_day, which is the numeric date rather than the day of the week)
|
464
|
+
# des_facet Array (Strings) X X Descriptive subject terms assigned by Times indexers
|
465
|
+
#
|
466
|
+
# When used in a request, values must be UPPERCASE
|
467
|
+
# desk_facet
|
468
|
+
# desk_facet String X X The Times desk that produced the story (e.g., Business/Financial Desk)
|
469
|
+
# fee Boolean X X Indicates whether users must pay a fee to retrieve the full article
|
470
|
+
# geo_facet Array (Strings) X X Standardized names of geographic locations, assigned by Times indexers
|
471
|
+
#
|
472
|
+
# When used in a request, values must be UPPERCASE
|
473
|
+
# lead_paragraph String X X The first paragraph of the article (as it appeared in the printed newspaper)
|
474
|
+
# material_type_facet Array (Strings) X X The general article type, such as Biography, Editorial or Review
|
475
|
+
# multimedia Array X Associated multimedia features, including URLs (see also the related_multimedia field)
|
476
|
+
# nytd_byline_facet String X X The article byline, formatted for NYTimes.com
|
477
|
+
# nytd_des_facet Array (Strings) X X Descriptive subject terms, assigned for use on NYTimes.com (to get standardized terms, use the TimesTags API)
|
478
|
+
#
|
479
|
+
# When used in a request, values must be Mixed Case
|
480
|
+
# nytd_geo_facet Array (Strings) X X Standardized names of geographic locations, assigned for use on NYTimes.com (to get standardized terms, use the TimesTags API)
|
481
|
+
#
|
482
|
+
# When used in a request, values must be Mixed Case
|
483
|
+
# nytd_lead_paragraph String X X The first paragraph of the article (as it appears on NYTimes.com)
|
484
|
+
# nytd_org_facet Array (Strings) X X Standardized names of organizations, assigned for use on NYTimes.com (to get standardized terms, use the TimesTags API)
|
485
|
+
#
|
486
|
+
# When used in a request, values must be Mixed Case
|
487
|
+
# nytd_per_facet Array (Strings) X X Standardized names of people, assigned for use on NYTimes.com (to get standardized terms, use the TimesTags API)
|
488
|
+
#
|
489
|
+
# When used in a request, values must be Mixed Case
|
490
|
+
# nytd_section_facet Array (Strings) X X The section the article appears in (on NYTimes.com)
|
491
|
+
# nytd_title String X X The article title on NYTimes.com (this field may or may not match the title field; headlines may be shortened and edited for the Web)
|
492
|
+
# nytd_works_mentioned
|
493
|
+
# _facet String X X Literary works mentioned (titles formatted for use on NYTimes.com)
|
494
|
+
# org_facet Array (Strings) X X Standardized names of organizations, assigned by Times indexers
|
495
|
+
#
|
496
|
+
# When used in a request, values must be UPPERCASE
|
497
|
+
# page_facet String X X The page the article appeared on (in the printed paper)
|
498
|
+
# per_facet Array (Strings) X X Standardized names of people, assigned by Times indexers
|
499
|
+
#
|
500
|
+
# When used in a request, values must be UPPERCASE
|
501
|
+
# publication_day
|
502
|
+
# publication_month
|
503
|
+
# publication_year Date
|
504
|
+
# Date
|
505
|
+
# Date X
|
506
|
+
# X
|
507
|
+
# X X
|
508
|
+
# x
|
509
|
+
# x The day (DD), month (MM) and year (YYYY) segments of date, separated for use as facets
|
510
|
+
# related_multimedia Boolean X X Indicates whether multimedia features are associated with this article. Additional metadata for each related multimedia feature appears in the multimedia array.
|
511
|
+
# section_page_facet String X X The full page number of the printed article (e.g., D00002)
|
512
|
+
# small_image
|
513
|
+
# small_image_url
|
514
|
+
# small_image_height
|
515
|
+
# small_image_width Boolean
|
516
|
+
# String
|
517
|
+
# Integer
|
518
|
+
# Integer X X
|
519
|
+
# X
|
520
|
+
# X
|
521
|
+
# X The small_image field indicates whether a smaller thumbnail image is associated with the article. The small_image_url field provides the URL of the image on NYTimes.com. The small_image_height and small_image_width fields provide the image dimensions.
|
522
|
+
# source_facet String X X The originating body (e.g., AP, Dow Jones, The New York Times)
|
523
|
+
# text String X The text field consists of title + byline + body (combined in an OR search) and is the default field for keyword searches. For more information, see Constructing a Search Query.
|
524
|
+
# title String X X The article title (headline); corresponds to the headline that appeared in the printed newspaper
|
525
|
+
# url String X X The URL of the article on NYTimes.com
|
526
|
+
# word_count Integer X The full article word count
|
527
|
+
# works_mentioned_facet
|