taylorbarstow-nytimes-articles 0.2.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 +6 -0
- data/lib/nytimes_articles/article.rb +462 -0
- data/lib/nytimes_articles/base.rb +118 -0
- data/lib/nytimes_articles/exceptions.rb +38 -0
- data/lib/nytimes_articles/facet.rb +128 -0
- data/lib/nytimes_articles/query.rb +28 -0
- data/lib/nytimes_articles/result_set.rb +66 -0
- data/lib/nytimes_articles/thumbnail.rb +30 -0
- data/test/nytimes/articles/test_article.rb +640 -0
- data/test/nytimes/articles/test_base.rb +92 -0
- data/test/nytimes/articles/test_facet.rb +109 -0
- data/test/nytimes/articles/test_query.rb +89 -0
- data/test/nytimes/articles/test_result_set.rb +62 -0
- data/test/nytimes/articles/test_thumbnail.rb +47 -0
- data/test/test_helper.rb +31 -0
- metadata +82 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'json'
|
3
|
+
require 'htmlentities'
|
4
|
+
|
5
|
+
module Nytimes
|
6
|
+
module Articles
|
7
|
+
class Base
|
8
|
+
API_SERVER = 'api.nytimes.com'
|
9
|
+
API_VERSION = 'v1'
|
10
|
+
API_NAME = 'article'
|
11
|
+
API_BASE = "/svc/search/#{API_VERSION}/#{API_NAME}"
|
12
|
+
|
13
|
+
@@api_key = nil
|
14
|
+
@@debug = false
|
15
|
+
|
16
|
+
##
|
17
|
+
# Set the API key used for operations. This needs to be called before any requests against the API. To obtain an API key, go to http://developer.nytimes.com/
|
18
|
+
def self.api_key=(key)
|
19
|
+
@@api_key = key
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.debug=(flag)
|
23
|
+
@@debug = flag
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Returns the current value of the API Key
|
28
|
+
def self.api_key
|
29
|
+
@@api_key
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Builds a request URI to call the API server
|
34
|
+
def self.build_request_url(params)
|
35
|
+
URI::HTTP.build :host => API_SERVER,
|
36
|
+
:path => API_BASE,
|
37
|
+
:query => params.map {|k,v| "#{URI.escape(k)}=#{URI.escape(v)}"}.join('&')
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.text_field(value)
|
41
|
+
return nil if value.nil?
|
42
|
+
coder = HTMLEntities.new
|
43
|
+
coder.decode(value)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.integer_field(value)
|
47
|
+
return nil if value.nil?
|
48
|
+
value.to_i
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.date_field(value)
|
52
|
+
return nil unless value =~ /^\d{8}$/
|
53
|
+
Date.strptime(value, "%Y%m%d")
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.boolean_field(value)
|
57
|
+
case value
|
58
|
+
when nil
|
59
|
+
false
|
60
|
+
when TrueClass
|
61
|
+
true
|
62
|
+
when FalseClass
|
63
|
+
false
|
64
|
+
when 'Y'
|
65
|
+
true
|
66
|
+
when 'N'
|
67
|
+
false
|
68
|
+
else
|
69
|
+
false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.invoke(params={})
|
74
|
+
begin
|
75
|
+
if @@api_key.nil?
|
76
|
+
raise AuthenticationError, "You must initialize the API key before you run any API queries"
|
77
|
+
end
|
78
|
+
|
79
|
+
full_params = params.merge 'api-key' => @@api_key
|
80
|
+
uri = build_request_url(full_params)
|
81
|
+
|
82
|
+
puts "REQUEST: #{uri}" if @@debug
|
83
|
+
|
84
|
+
reply = uri.read
|
85
|
+
parsed_reply = JSON.parse reply
|
86
|
+
|
87
|
+
if parsed_reply.nil?
|
88
|
+
raise BadResponseError, "Empty reply returned from API"
|
89
|
+
end
|
90
|
+
|
91
|
+
#case parsed_reply['status']
|
92
|
+
# FIXME
|
93
|
+
#end
|
94
|
+
|
95
|
+
parsed_reply
|
96
|
+
rescue OpenURI::HTTPError => e
|
97
|
+
# FIXME: Return message from body?
|
98
|
+
case e.message
|
99
|
+
when /^400/
|
100
|
+
raise BadRequestError
|
101
|
+
when /^403/
|
102
|
+
raise AuthenticationError
|
103
|
+
when /^404/
|
104
|
+
return nil
|
105
|
+
when /^500/
|
106
|
+
raise ServerError
|
107
|
+
else
|
108
|
+
raise ConnectionError
|
109
|
+
end
|
110
|
+
|
111
|
+
raise "Error connecting to URL #{uri} #{e}"
|
112
|
+
rescue JSON::ParserError => e
|
113
|
+
raise BadResponseError, "Invalid JSON returned from API:\n#{reply}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Nytimes
|
2
|
+
module Articles
|
3
|
+
##
|
4
|
+
# The generic Error class from which all other Errors are derived.
|
5
|
+
class Error < ::RuntimeError
|
6
|
+
end
|
7
|
+
|
8
|
+
##
|
9
|
+
# This error is thrown if there are problems authenticating your API key.
|
10
|
+
class AuthenticationError < Error
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# This error is thrown if the request was not parsable by the API server.
|
15
|
+
class BadRequestError < Error
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# This error is thrown if the response from the API server is not parsable.
|
20
|
+
class BadResponseError < Error
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# This error is thrown if there is an error connecting to the API server.
|
25
|
+
class ServerError < Error
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# This error is thrown if there is a timeout connecting to the server (to be implemented).
|
30
|
+
class TimeoutError < Error
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# This error is thrown for general connection errors to the API server.
|
35
|
+
class ConnectionError < Error
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module Nytimes
|
2
|
+
module Articles
|
3
|
+
|
4
|
+
##
|
5
|
+
# This class represents a Facet used in the ArticleSearch API. Facets can be used to both search for matching articles (see Article#search) and
|
6
|
+
# are also returned as article and search metadata. Facets are made up of 3 parts:
|
7
|
+
# * <tt>facet_type</tt> - a string; see Article#search for a list of facet types
|
8
|
+
# * <tt>term</tt> - a string as well
|
9
|
+
# * <tt>count</tt> - Facets returned as search metadata (via the <tt>:facets</tt> parameter to Article#search) also include a non-nil count of matching articles for that facet
|
10
|
+
class Facet
|
11
|
+
##
|
12
|
+
# The term for the facet
|
13
|
+
attr_reader :term
|
14
|
+
|
15
|
+
##
|
16
|
+
# The number of times this facet has appeared in the search results (note: this only applies for facets returned in the facets header on an Article#search)
|
17
|
+
attr_reader :count
|
18
|
+
|
19
|
+
##
|
20
|
+
# The facet type
|
21
|
+
attr_reader :facet_type
|
22
|
+
|
23
|
+
# Facet name constants
|
24
|
+
CLASSIFIERS = 'classifiers_facet'
|
25
|
+
COLUMN = 'column_facet'
|
26
|
+
DATE = 'date'
|
27
|
+
DAY_OF_WEEK = 'day_of_week_facet'
|
28
|
+
DESCRIPTION = 'des_facet'
|
29
|
+
DESK = 'desk_facet'
|
30
|
+
GEO = 'geo_facet'
|
31
|
+
MATERIAL_TYPE = 'material_type_facet'
|
32
|
+
ORGANIZATION = 'org_facet'
|
33
|
+
PAGE = 'page_facet'
|
34
|
+
PERSON = 'per_facet'
|
35
|
+
PUB_DAY = 'publication_day'
|
36
|
+
PUB_MONTH = 'publication_month'
|
37
|
+
PUB_YEAR = 'publication_year'
|
38
|
+
SECTION_PAGE = 'section_page_facet'
|
39
|
+
SOURCE = 'source_facet'
|
40
|
+
WORKS_MENTIONED = 'works_mentioned_facet'
|
41
|
+
|
42
|
+
# Facets of content formatted for nytimes.com
|
43
|
+
NYTD_BYLINE = 'nytd_byline'
|
44
|
+
NYTD_DESCRIPTION = 'nytd_des_facet'
|
45
|
+
NYTD_GEO = 'nytd_geo_facet'
|
46
|
+
NYTD_ORGANIZATION = 'nytd_org_facet'
|
47
|
+
NYTD_PERSON = 'nytd_per_facet'
|
48
|
+
NYTD_SECTION = 'nytd_section_facet'
|
49
|
+
NYTD_WORKS_MENTIONED = 'nytd_works_mentioned_facet'
|
50
|
+
|
51
|
+
# The default 5 facets to return
|
52
|
+
DEFAULT_RETURN_FACETS = [DESCRIPTION, GEO, ORGANIZATION, PERSON, DESK]
|
53
|
+
|
54
|
+
ALL_FACETS = [CLASSIFIERS, COLUMN, DATE, DAY_OF_WEEK, DESCRIPTION, DESK, GEO, MATERIAL_TYPE, ORGANIZATION, PAGE, PERSON, PUB_DAY,
|
55
|
+
PUB_MONTH, PUB_YEAR, SECTION_PAGE, SOURCE, WORKS_MENTIONED, NYTD_BYLINE, NYTD_DESCRIPTION, NYTD_GEO,
|
56
|
+
NYTD_ORGANIZATION, NYTD_PERSON, NYTD_SECTION, NYTD_WORKS_MENTIONED]
|
57
|
+
|
58
|
+
##
|
59
|
+
# Initializes the facet. There is seldom a reason for you to call this.
|
60
|
+
def initialize(facet_type, term, count)
|
61
|
+
@facet_type = facet_type
|
62
|
+
@term = term
|
63
|
+
@count = count
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Takes a symbol name and subs it to a string constant
|
68
|
+
def self.symbol_name(facet)
|
69
|
+
case facet
|
70
|
+
when String
|
71
|
+
return facet
|
72
|
+
when Facet
|
73
|
+
return facet.facet_type
|
74
|
+
when Symbol
|
75
|
+
# fall through
|
76
|
+
else
|
77
|
+
raise ArgumentError, "Unsupported type to Facet#symbol_to_api_name"
|
78
|
+
end
|
79
|
+
|
80
|
+
case facet
|
81
|
+
when :geography
|
82
|
+
GEO
|
83
|
+
when :org, :orgs
|
84
|
+
ORGANIZATION
|
85
|
+
when :people
|
86
|
+
PERSON
|
87
|
+
when :nytd_geography
|
88
|
+
NYTD_GEO
|
89
|
+
when :nytd_org, :nytd_orgs
|
90
|
+
NYTD_ORGANIZATION
|
91
|
+
when :nytd_people
|
92
|
+
NYTD_PERSON
|
93
|
+
else
|
94
|
+
name = facet.to_s.upcase
|
95
|
+
|
96
|
+
if const_defined?(name)
|
97
|
+
const_get(name)
|
98
|
+
elsif name =~ /S$/ && const_defined?(name.gsub(/S$/, ''))
|
99
|
+
const_get(name.gsub(/S$/, ''))
|
100
|
+
else
|
101
|
+
raise ArgumentError, "Unable to find a matching facet key for symbol :#{facet}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# Initializes a selection of Facet objects returned from the API. Used for marshaling Facets in articles and metadata from search results
|
108
|
+
# (Note: some facets are returned as scalar values)
|
109
|
+
def self.init_from_api(api_hash)
|
110
|
+
return nil if api_hash.nil?
|
111
|
+
|
112
|
+
unless api_hash.is_a? Hash
|
113
|
+
raise ArgumentError, "expecting a Hash only"
|
114
|
+
else
|
115
|
+
return nil if api_hash.empty?
|
116
|
+
end
|
117
|
+
|
118
|
+
out = {}
|
119
|
+
|
120
|
+
api_hash.each_pair do |k,v|
|
121
|
+
out[k] = v.map {|f| Facet.new(k, f['term'], f['count'])}
|
122
|
+
end
|
123
|
+
|
124
|
+
out
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module Nytimes
|
4
|
+
module Articles
|
5
|
+
##
|
6
|
+
# The Query class represents a single query to the Article Search API. Supports
|
7
|
+
# all of the named parameters to Article.search as accessor methods.
|
8
|
+
#
|
9
|
+
class Query
|
10
|
+
FIELDS = [:only_facets, :except_facets, :begin_date, :end_date, :since,
|
11
|
+
:before, :fee, :has_thumbnail, :facets, :fields, :query, :offset] + Article::TEXT_FIELDS.map(&:to_sym)
|
12
|
+
FIELDS.each {|f| attr_accessor f}
|
13
|
+
|
14
|
+
# Produce a hash which uniquely identifies this query
|
15
|
+
def hash
|
16
|
+
strs = FIELDS.collect {|f| "#{f}:#{send(f).inspect}"}
|
17
|
+
Digest::SHA256.hexdigest(strs.join(' '))
|
18
|
+
end
|
19
|
+
|
20
|
+
# Perform this query. Returns result of Article.search
|
21
|
+
def perform
|
22
|
+
params = {}
|
23
|
+
FIELDS.each {|f| params[f] = send(f) unless send(f).nil?}
|
24
|
+
Article.search(params)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Nytimes
|
5
|
+
module Articles
|
6
|
+
##
|
7
|
+
# The ResultSet is returned by Article#search and contains an array of up to 10 results out of the total matches. For your convenience, this
|
8
|
+
# object provides a selection of array methods on the underlying collection of articles.
|
9
|
+
class ResultSet < Base
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
##
|
13
|
+
# The offset of the result_set. Note that this is essentially the ordinal position of the batch among all results. First 10 results are offset
|
14
|
+
# 0, the next 10 are offset 1, etc.
|
15
|
+
attr_reader :offset
|
16
|
+
|
17
|
+
##
|
18
|
+
# The total results that matched the query.
|
19
|
+
attr_reader :total_results
|
20
|
+
|
21
|
+
##
|
22
|
+
# The results array of articles returned. Note that if you call Articles#find with :fields => :none, this will return nil even if
|
23
|
+
# there are matching results.
|
24
|
+
attr_reader :results
|
25
|
+
|
26
|
+
##
|
27
|
+
# If you have specified a list of <tt>:facets</tt> for Article#search, they will be returned in a hash keyed by the facet name here.
|
28
|
+
attr_reader :facets
|
29
|
+
|
30
|
+
BATCH_SIZE = 10
|
31
|
+
|
32
|
+
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
|
33
|
+
|
34
|
+
def initialize(params)
|
35
|
+
@offset = params[:offset]
|
36
|
+
@total_results = params[:total_results]
|
37
|
+
@results = params[:results]
|
38
|
+
@facets = params[:facets]
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# For your convenience, the page_number method is an alternate version of #offset that counts up from 1.
|
43
|
+
def page_number
|
44
|
+
return 0 if @total_results == 0
|
45
|
+
@offset + 1
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Calculates the total number of pages in the results based on the standard batch size and total results.
|
50
|
+
def total_pages
|
51
|
+
return 0 if @total_results == 0
|
52
|
+
(@total_results.to_f / BATCH_SIZE).ceil
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Used to initialize a new result_set from Article#search.
|
57
|
+
def self.init_from_api(api_hash)
|
58
|
+
self.new(:offset => integer_field(api_hash['offset']),
|
59
|
+
:total_results => integer_field(api_hash['total']),
|
60
|
+
:results => api_hash['results'].map {|r| Article.init_from_api(r)},
|
61
|
+
:facets => Facet.init_from_api(api_hash['facets'])
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Nytimes
|
2
|
+
module Articles
|
3
|
+
##
|
4
|
+
# If requested in <tt>:fields</tt> for an article search, some articles are returned with a matching thumbnail image. The several thumbnail
|
5
|
+
# fields are collected together into a single Thumbnail instance for your convenience.
|
6
|
+
class Thumbnail
|
7
|
+
attr_reader :url, :width, :height
|
8
|
+
|
9
|
+
def initialize(url, width, height)
|
10
|
+
@url = url
|
11
|
+
@width = width
|
12
|
+
@height = height
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.init_from_api(api_hash)
|
16
|
+
return nil unless !api_hash.nil? && api_hash['small_image_url']
|
17
|
+
|
18
|
+
unless api_hash['small_image_width'].nil?
|
19
|
+
width = api_hash['small_image_width'].to_i
|
20
|
+
end
|
21
|
+
|
22
|
+
unless api_hash['small_image_height'].nil?
|
23
|
+
height = api_hash['small_image_height'].to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
new(api_hash['small_image_url'], width, height)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,640 @@
|
|
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 "send the :before value through as a end_date" do
|
61
|
+
t = Time.now
|
62
|
+
Article.expects(:invoke).with(has_entry('end_date', t.strftime("%Y%m%d")))
|
63
|
+
Article.search :before => t
|
64
|
+
end
|
65
|
+
|
66
|
+
should "not send through :before as an argument to the API" do
|
67
|
+
t = Time.now
|
68
|
+
Article.expects(:invoke).with(Not(has_key('before')))
|
69
|
+
Article.search :before => t
|
70
|
+
end
|
71
|
+
|
72
|
+
should "raise an ArgumentError if the before_date is NOT a string and does not respond_to strftime" do
|
73
|
+
assert_raise(ArgumentError) { Article.search :before => 23 }
|
74
|
+
end
|
75
|
+
|
76
|
+
should "add a begin_date in 1980 if no :since or :begin_date argument is provided" do
|
77
|
+
Article.expects(:invoke).with(has_entry('begin_date', Article::EARLIEST_BEGIN_DATE))
|
78
|
+
Article.search :before => Time.now
|
79
|
+
end
|
80
|
+
|
81
|
+
should "not automatically add a begin_date is there is a :since argument" do
|
82
|
+
since = Time.now - 12000
|
83
|
+
Article.expects(:invoke).with(has_entry('begin_date', since.strftime("%Y%m%d")))
|
84
|
+
Article.search :before => Time.now, :since => since
|
85
|
+
end
|
86
|
+
|
87
|
+
should "not automatically add a begin_date if there is a :begin_date argument already" do
|
88
|
+
since = Time.now - 12000
|
89
|
+
Article.expects(:invoke).with(has_entry('begin_date', since.strftime("%Y%m%d")))
|
90
|
+
Article.search :before => Time.now, :begin_date => since
|
91
|
+
end
|
92
|
+
|
93
|
+
should "raise an ArgumentError if there is also an :end_date argument" do
|
94
|
+
assert_raise(ArgumentError) { Article.search :before => Time.now, :end_date => Time.now }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context ":since" do
|
99
|
+
should "send the :since value through as a begin_date" do
|
100
|
+
t = Time.now - 1200
|
101
|
+
Article.expects(:invoke).with(has_entry('begin_date', t.strftime("%Y%m%d")))
|
102
|
+
Article.search :since => t
|
103
|
+
end
|
104
|
+
|
105
|
+
should "not send through :since as an argument to the API" do
|
106
|
+
t = Time.now
|
107
|
+
Article.expects(:invoke).with(Not(has_key('since')))
|
108
|
+
Article.search :since => t
|
109
|
+
end
|
110
|
+
|
111
|
+
should "raise an ArgumentError if the before_date is NOT a string and does not respond_to strftime" do
|
112
|
+
assert_raise(ArgumentError) { Article.search :since => 23 }
|
113
|
+
end
|
114
|
+
|
115
|
+
# This is to fix an error where the begin and end date are the same
|
116
|
+
should "add a end_date of tomorrow if no :before or :end_date argument is provided" do
|
117
|
+
Article.expects(:invoke).with(has_entry('end_date', (Date.today + 1).strftime("%Y%m%d")))
|
118
|
+
Article.search :since => Date.today
|
119
|
+
end
|
120
|
+
|
121
|
+
should "not automatically add a end_date is there is a :before argument" do
|
122
|
+
since = '19990101'
|
123
|
+
Article.expects(:invoke).with(has_entry('end_date', '20030101'))
|
124
|
+
Article.search :before => '20030101', :since => since
|
125
|
+
end
|
126
|
+
|
127
|
+
should "not automatically add a end_date if there is a :end_date argument already" do
|
128
|
+
since = '19990101'
|
129
|
+
Article.expects(:invoke).with(has_entry('end_date', '20030101'))
|
130
|
+
Article.search :end_date => '20030101', :since => since
|
131
|
+
end
|
132
|
+
|
133
|
+
should "raise an ArgumentError if there is also an :begin_date argument" do
|
134
|
+
assert_raise(ArgumentError) { Article.search :since => Time.now, :begin_date => Time.now }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context "facets" do
|
140
|
+
should "accept a single string" do
|
141
|
+
Article.expects(:invoke).with(has_entry("facets", Facet::DATE))
|
142
|
+
Article.search "FOO BAR", :facets => Facet::DATE
|
143
|
+
end
|
144
|
+
|
145
|
+
should "accept an array of strings" do
|
146
|
+
Article.expects(:invoke).with(has_entry("facets", [Facet::DATE, Facet::GEO].join(',')))
|
147
|
+
Article.search "FOO BAR", :facets => [Facet::DATE, Facet::GEO]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
context "only_facets" do
|
152
|
+
should "accept a String" do
|
153
|
+
Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA]"))
|
154
|
+
Article.search :only_facets => "#{Facet::GEO}:[CALIFORNIA]"
|
155
|
+
end
|
156
|
+
|
157
|
+
should "accept a single hash value Facet string to a term" do
|
158
|
+
Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA]"))
|
159
|
+
Article.search :only_facets => {Facet::GEO => 'CALIFORNIA'}
|
160
|
+
end
|
161
|
+
|
162
|
+
should "accept an Facet string hashed to an array terms" do
|
163
|
+
Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA] #{Facet::GEO}:[GREAT BRITAIN]"))
|
164
|
+
Article.search :only_facets => {Facet::GEO => ['CALIFORNIA', 'GREAT BRITAIN']}
|
165
|
+
end
|
166
|
+
|
167
|
+
should "accept a single Facet object" do
|
168
|
+
f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
|
169
|
+
Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA]"))
|
170
|
+
Article.search :only_facets => f
|
171
|
+
end
|
172
|
+
|
173
|
+
should "accept an array of Facet objects" do
|
174
|
+
f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
|
175
|
+
f2 = Facet.new(Facet::NYTD_ORGANIZATION, 'University Of California', 12)
|
176
|
+
|
177
|
+
Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA] #{Facet::NYTD_ORGANIZATION}:[University Of California]"))
|
178
|
+
Article.search :only_facets => [f, f2]
|
179
|
+
end
|
180
|
+
|
181
|
+
should "merge multiple Facets objects in the array of the same type into one array" do
|
182
|
+
f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
|
183
|
+
f2 = Facet.new(Facet::GEO, 'IOWA', 12)
|
184
|
+
|
185
|
+
Article.expects(:invoke).with(has_entry("query", "#{Facet::GEO}:[CALIFORNIA] #{Facet::GEO}:[IOWA]"))
|
186
|
+
Article.search :only_facets => [f, f2]
|
187
|
+
end
|
188
|
+
|
189
|
+
should "not stomp on an existing query string" do
|
190
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream #{Facet::GEO}:[CALIFORNIA]"))
|
191
|
+
Article.search "ice cream", :only_facets => {Facet::GEO => "CALIFORNIA"}
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
context "except_facets" do
|
196
|
+
should "accept a String" do
|
197
|
+
Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA]"))
|
198
|
+
Article.search :except_facets => "-#{Facet::GEO}:[CALIFORNIA]"
|
199
|
+
end
|
200
|
+
|
201
|
+
should "accept a single hash value Facet string to a term" do
|
202
|
+
Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA]"))
|
203
|
+
Article.search :except_facets => {Facet::GEO => 'CALIFORNIA'}
|
204
|
+
end
|
205
|
+
|
206
|
+
should "accept an Facet string hashed to an array terms" do
|
207
|
+
Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA] -#{Facet::GEO}:[GREAT BRITAIN]"))
|
208
|
+
Article.search :except_facets => {Facet::GEO => ['CALIFORNIA', 'GREAT BRITAIN']}
|
209
|
+
end
|
210
|
+
|
211
|
+
should "accept a single Facet object" do
|
212
|
+
f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
|
213
|
+
Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA]"))
|
214
|
+
Article.search :except_facets => f
|
215
|
+
end
|
216
|
+
|
217
|
+
should "accept an array of Facet objects" do
|
218
|
+
f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
|
219
|
+
f2 = Facet.new(Facet::NYTD_ORGANIZATION, 'University Of California', 12)
|
220
|
+
|
221
|
+
Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA] -#{Facet::NYTD_ORGANIZATION}:[University Of California]"))
|
222
|
+
Article.search :except_facets => [f, f2]
|
223
|
+
end
|
224
|
+
|
225
|
+
should "merge multiple Facets objects in the array of the same type into one array" do
|
226
|
+
f = Facet.new(Facet::GEO, 'CALIFORNIA', 2394)
|
227
|
+
f2 = Facet.new(Facet::GEO, 'IOWA', 12)
|
228
|
+
|
229
|
+
Article.expects(:invoke).with(has_entry("query", "-#{Facet::GEO}:[CALIFORNIA] -#{Facet::GEO}:[IOWA]"))
|
230
|
+
Article.search :except_facets => [f, f2]
|
231
|
+
end
|
232
|
+
|
233
|
+
should "not stomp on an existing query string" do
|
234
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream -#{Facet::GEO}:[CALIFORNIA]"))
|
235
|
+
Article.search "ice cream", :except_facets => {Facet::GEO => "CALIFORNIA"}
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
context ":fee" do
|
240
|
+
should "send through as fee:Y if set to true" do
|
241
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream fee:Y"))
|
242
|
+
Article.search "ice cream", :fee => true
|
243
|
+
end
|
244
|
+
|
245
|
+
should "send through as -fee:Y if set to false" do
|
246
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream -fee:Y"))
|
247
|
+
Article.search "ice cream", :fee => false
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
context ":fields" do
|
252
|
+
context "for the :all argument" do
|
253
|
+
should "pass all fields in a comma-delimited list" do
|
254
|
+
Article.expects(:invoke).with(has_entry('fields', Article::ALL_FIELDS.join(',')))
|
255
|
+
Article.search "FOO BAR", :fields => :all
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
context "for the :none argument" do
|
260
|
+
should "request a blank space for the fields argument" do
|
261
|
+
Article.expects(:invoke).with(has_entry('fields', ' '))
|
262
|
+
Article.search "FOO BAR", :fields => :none
|
263
|
+
end
|
264
|
+
|
265
|
+
should "request the standard :facets if no :facets have been explicitly provided" do
|
266
|
+
Article.expects(:invoke).with(has_entry('facets', Facet::DEFAULT_RETURN_FACETS.join(',')))
|
267
|
+
Article.search "FOO BAR", :fields => :none
|
268
|
+
end
|
269
|
+
|
270
|
+
should "request the given :facets field if provided" do
|
271
|
+
Article.expects(:invoke).with(has_entry('facets', "#{Facet::GEO}"))
|
272
|
+
Article.search "FOO BAR", :fields => :none, :facets => Facet::GEO
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
context ":thumbnail" do
|
277
|
+
should "accept the symbol version of the argument" do
|
278
|
+
Article.expects(:invoke).with(has_entry('fields', Article::IMAGE_FIELDS.join(',')))
|
279
|
+
Article.search "FOO BAR", :fields => :thumbnail
|
280
|
+
end
|
281
|
+
|
282
|
+
should "accept the string version of the argument" do
|
283
|
+
Article.expects(:invoke).with(has_entry('fields', Article::IMAGE_FIELDS.join(',')))
|
284
|
+
Article.search "FOO BAR", :fields => 'thumbnail'
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
context ":multimedia" do
|
289
|
+
should "be implemented"
|
290
|
+
end
|
291
|
+
|
292
|
+
should "accept a single string as an argument" do
|
293
|
+
Article.expects(:invoke).with(has_entry('fields', 'body'))
|
294
|
+
Article.search "FOO BAR", :fields => 'body'
|
295
|
+
end
|
296
|
+
|
297
|
+
should "accept a single symbol as an argument" do
|
298
|
+
Article.expects(:invoke).with(has_entry('fields', 'body'))
|
299
|
+
Article.search "FOO BAR", :fields => :body
|
300
|
+
end
|
301
|
+
|
302
|
+
should "accept an array of strings and symbols" do
|
303
|
+
Article.expects(:invoke).with(has_entry('fields', 'abstract,body'))
|
304
|
+
Article.search "FOO BAR", :fields => [:abstract, 'body']
|
305
|
+
end
|
306
|
+
|
307
|
+
should "raise an ArgumentError otherwise" do
|
308
|
+
assert_raise(ArgumentError) { Article.search :fields => 12 }
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
context ":has_multimedia" do
|
313
|
+
should "send through as related_multimedia:Y if set to true" do
|
314
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream related_multimedia:Y"))
|
315
|
+
Article.search "ice cream", :has_multimedia => true
|
316
|
+
end
|
317
|
+
|
318
|
+
should "send through as -related_multimedia:Y if set to false" do
|
319
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream -related_multimedia:Y"))
|
320
|
+
Article.search "ice cream", :has_multimedia => false
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
context ":has_thumbnail" do
|
325
|
+
should "send through as small_image:Y if set to true" do
|
326
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream small_image:Y"))
|
327
|
+
Article.search "ice cream", :has_thumbnail => true
|
328
|
+
end
|
329
|
+
|
330
|
+
should "send through as -small_image:Y if set to false" do
|
331
|
+
Article.expects(:invoke).with(has_entry("query", "ice cream -small_image:Y"))
|
332
|
+
Article.search "ice cream", :has_thumbnail => false
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
context ":offset" do
|
337
|
+
should "pass through an explicit offset parameter if specified" do
|
338
|
+
Article.expects(:invoke).with(has_entry("offset", 10))
|
339
|
+
Article.search :offset => 10
|
340
|
+
end
|
341
|
+
|
342
|
+
should "raise an ArgumentError if the offset is not an Integer" do
|
343
|
+
assert_raise(ArgumentError) { Article.search :offset => 'apple' }
|
344
|
+
end
|
345
|
+
|
346
|
+
should "pass through an offset of page - 1 if :page is used instead" do
|
347
|
+
Article.expects(:invoke).with(has_entry("offset", 2))
|
348
|
+
Article.search :page => 3
|
349
|
+
end
|
350
|
+
|
351
|
+
should "not pass through a page parameter to the API" do
|
352
|
+
Article.expects(:invoke).with(Not(has_key("page")))
|
353
|
+
Article.search :page => 3
|
354
|
+
end
|
355
|
+
|
356
|
+
should "raise an ArgumentError if the page is not an Integer" do
|
357
|
+
assert_raise(ArgumentError) { Article.search :page => 'orange' }
|
358
|
+
end
|
359
|
+
|
360
|
+
should "raise an ArgumentError if the page is less than 1" do
|
361
|
+
assert_raise(ArgumentError) { Article.search :page => 0 }
|
362
|
+
end
|
363
|
+
|
364
|
+
should "use the :offset argument if both an :offset and :page are provided" do
|
365
|
+
Article.expects(:invoke).with(has_entry("offset", 2))
|
366
|
+
Article.search :offset => 2, :page => 203
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
context "rank" do
|
371
|
+
%w(newest oldest closest).each do |rank|
|
372
|
+
should "accept #{rank} as the argument to rank" do
|
373
|
+
Article.expects(:invoke).with(has_entry("rank", rank))
|
374
|
+
Article.search :rank => rank.to_sym
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
should "raise an ArgumentError if rank is something else" do
|
379
|
+
assert_raise(ArgumentError) { Article.search :rank => :clockwise }
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
Article::TEXT_FIELDS.each do |tf|
|
384
|
+
context ":#{tf} parameter" do
|
385
|
+
should "prefix each non-quoted term with the #{tf}: field identifier in the query to the API" do
|
386
|
+
Article.expects(:invoke).with(has_entry("query", "#{tf}:ice #{tf}:cream"))
|
387
|
+
Article.search tf.to_sym => 'ice cream'
|
388
|
+
end
|
389
|
+
|
390
|
+
should "prefix -terms (excluded terms) with -#{tf}:" do
|
391
|
+
Article.expects(:invoke).with(has_entry("query", "#{tf}:ice -#{tf}:cream"))
|
392
|
+
Article.search tf.to_sym => 'ice -cream'
|
393
|
+
end
|
394
|
+
|
395
|
+
should "put quoted terms behind the field spec" do
|
396
|
+
Article.expects(:invoke).with(has_entry("query", "#{tf}:\"ice cream\" #{tf}:cone"))
|
397
|
+
Article.search tf.to_sym => '"ice cream" cone'
|
398
|
+
end
|
399
|
+
|
400
|
+
should "handle complicated combinations of expressions" do
|
401
|
+
Article.expects(:invoke).with(has_entry("query", "#{tf}:\"ice cream\" -#{tf}:cone #{tf}:\"waffle\""))
|
402
|
+
Article.search tf.to_sym => '"ice cream" -cone "waffle"'
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
# context "query parameters" do
|
408
|
+
# context "abstract" do
|
409
|
+
# should "be prefixed with the abstract: field identifier in the query"
|
410
|
+
# should "cast the argument to a string (will figure out processing later)"
|
411
|
+
# end
|
412
|
+
#
|
413
|
+
# context "author" do
|
414
|
+
# should "be prefixed with the author: field identifier in the query"
|
415
|
+
# should "cast the argument to a string (will figure out processing later)"
|
416
|
+
# end
|
417
|
+
#
|
418
|
+
# context "body" do
|
419
|
+
# should "be prefixed with the body: field identifier in the query"
|
420
|
+
# should "cast the argument to a string (will figure out processing later)"
|
421
|
+
# end
|
422
|
+
#
|
423
|
+
# context "byline" do
|
424
|
+
# should "be prefixed with the body: field identifier in the query"
|
425
|
+
# should "cast the argument to a string (will figure out processing later)"
|
426
|
+
# end
|
427
|
+
# end
|
428
|
+
end
|
429
|
+
|
430
|
+
context "Article.init_from_api" do
|
431
|
+
setup do
|
432
|
+
@article = Article.init_from_api(ARTICLE_API_HASH2)
|
433
|
+
end
|
434
|
+
|
435
|
+
Article::TEXT_FIELDS.each do |tf|
|
436
|
+
context "@#{tf}" do
|
437
|
+
should "read the value from the hash input" do
|
438
|
+
hash = {}
|
439
|
+
hash[tf] = "TEST TEXT"
|
440
|
+
article = Article.init_from_api(hash)
|
441
|
+
assert_equal "TEST TEXT", article.send(tf)
|
442
|
+
end
|
443
|
+
|
444
|
+
should "properly translate HTML entities back into characters" do
|
445
|
+
article = Article.init_from_api(tf => '“Money for Nothing”')
|
446
|
+
assert_equal "“Money for Nothing”", article.send(tf), article.inspect
|
447
|
+
end
|
448
|
+
|
449
|
+
should "only provide read-only access to the field" do
|
450
|
+
article = Article.init_from_api(tf => "TEST TEXT")
|
451
|
+
assert !article.respond_to?("#{tf}=")
|
452
|
+
end
|
453
|
+
|
454
|
+
should "return nil if the value is not provided in the hash" do
|
455
|
+
article = Article.init_from_api({"foo" => "bar"})
|
456
|
+
assert_nil article.send(tf)
|
457
|
+
end
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
Article::NUMERIC_FIELDS.each do |tf|
|
462
|
+
context "@#{tf}" do
|
463
|
+
should "read and coerce the string value from the hash input" do
|
464
|
+
article = Article.init_from_api(tf => "23")
|
465
|
+
assert_equal 23, article.send(tf)
|
466
|
+
end
|
467
|
+
|
468
|
+
should "only provide read-only access to the field" do
|
469
|
+
article = Article.init_from_api(tf => "23")
|
470
|
+
assert !article.respond_to?("#{tf}=")
|
471
|
+
end
|
472
|
+
|
473
|
+
should "return nil if the value is not provided in the hash" do
|
474
|
+
article = Article.init_from_api({"foo" => "bar"})
|
475
|
+
assert_nil article.send(tf)
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
# all the rest
|
481
|
+
context "@fee" do
|
482
|
+
setup do
|
483
|
+
@article = Article.init_from_api(ARTICLE_API_HASH)
|
484
|
+
end
|
485
|
+
|
486
|
+
should "be true if returned as true from the API" do
|
487
|
+
article = Article.init_from_api('fee' => true)
|
488
|
+
assert_equal true, article.fee?
|
489
|
+
assert_equal false, article.free?
|
490
|
+
end
|
491
|
+
|
492
|
+
should "be true if returned as Y from the API" do
|
493
|
+
article = Article.init_from_api('fee' => 'Y')
|
494
|
+
assert_equal true, article.fee?
|
495
|
+
assert_equal false, article.free?
|
496
|
+
end
|
497
|
+
|
498
|
+
should "default to false if not specified in the hash" do
|
499
|
+
assert_equal false, @article.fee?
|
500
|
+
assert_equal true, @article.free?
|
501
|
+
end
|
502
|
+
|
503
|
+
should "default to false if returned as N from the API" do
|
504
|
+
article = Article.init_from_api('fee' => 'N')
|
505
|
+
assert_equal false, article.fee?
|
506
|
+
assert_equal true, article.free?
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
context "@url" do
|
511
|
+
setup do
|
512
|
+
@article = Article.init_from_api(ARTICLE_API_HASH)
|
513
|
+
end
|
514
|
+
|
515
|
+
should "read the value from the hash" do
|
516
|
+
assert_equal ARTICLE_API_HASH['url'], @article.url
|
517
|
+
end
|
518
|
+
|
519
|
+
should "return a String" do
|
520
|
+
assert_kind_of(String, @article.url)
|
521
|
+
end
|
522
|
+
|
523
|
+
should "only provide read-only access to the field" do
|
524
|
+
assert !@article.respond_to?("url=")
|
525
|
+
end
|
526
|
+
|
527
|
+
should "return nil if the value is not provided in the hash" do
|
528
|
+
article = Article.init_from_api({"foo" => "bar"})
|
529
|
+
assert_nil article.url
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
context "@page" do
|
534
|
+
should "read the value from the page_facet field" do
|
535
|
+
assert_equal ARTICLE_API_HASH2['page_facet'].to_i, @article.page
|
536
|
+
end
|
537
|
+
|
538
|
+
should "only provide read-only access to the field" do
|
539
|
+
article = Article.new
|
540
|
+
assert !article.respond_to?("page=")
|
541
|
+
end
|
542
|
+
|
543
|
+
should "return nil if the value is not provided in the hash" do
|
544
|
+
article = Article.init_from_api({"foo" => "bar"})
|
545
|
+
assert_nil article.page
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
context "@thumbnail" do
|
550
|
+
should "assign nil to thumbnail otherwise" do
|
551
|
+
article = Article.init_from_api({"foo" => "bar"})
|
552
|
+
assert_nil article.thumbnail
|
553
|
+
end
|
554
|
+
|
555
|
+
should "create a thumbnail object if a small_image_url is part of the return hash" do
|
556
|
+
article = Article.init_from_api(ARTICLE_API_HASH2)
|
557
|
+
thumbnail = article.thumbnail
|
558
|
+
assert_not_nil thumbnail
|
559
|
+
assert_kind_of Thumbnail, thumbnail
|
560
|
+
assert_equal ARTICLE_API_HASH2['small_image_url'], thumbnail.url
|
561
|
+
assert_equal ARTICLE_API_HASH2['small_image_width'].to_i, thumbnail.width
|
562
|
+
assert_equal ARTICLE_API_HASH2['small_image_height'].to_i, thumbnail.height
|
563
|
+
end
|
564
|
+
end
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
|
569
|
+
# abstract String X X A summary of the article, written by Times indexers
|
570
|
+
# author String X X An author note, such as an e-mail address or short biography (compare byline)
|
571
|
+
# 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.
|
572
|
+
# byline String X X The article byline, including the author's name
|
573
|
+
# classifers_facet Array (Strings) X X Taxonomic classifiers that reflect Times content categories, such as Top/News/Sports
|
574
|
+
# column_facet String X X A Times column title (if applicable), such as Weddings or Ideas & Trends
|
575
|
+
# date Date X X The publication date in YYYYMMDD format
|
576
|
+
# 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)
|
577
|
+
# des_facet Array (Strings) X X Descriptive subject terms assigned by Times indexers
|
578
|
+
#
|
579
|
+
# When used in a request, values must be UPPERCASE
|
580
|
+
# desk_facet
|
581
|
+
# desk_facet String X X The Times desk that produced the story (e.g., Business/Financial Desk)
|
582
|
+
# fee Boolean X X Indicates whether users must pay a fee to retrieve the full article
|
583
|
+
# geo_facet Array (Strings) X X Standardized names of geographic locations, assigned by Times indexers
|
584
|
+
#
|
585
|
+
# When used in a request, values must be UPPERCASE
|
586
|
+
# lead_paragraph String X X The first paragraph of the article (as it appeared in the printed newspaper)
|
587
|
+
# material_type_facet Array (Strings) X X The general article type, such as Biography, Editorial or Review
|
588
|
+
# multimedia Array X Associated multimedia features, including URLs (see also the related_multimedia field)
|
589
|
+
# nytd_byline_facet String X X The article byline, formatted for NYTimes.com
|
590
|
+
# nytd_des_facet Array (Strings) X X Descriptive subject terms, assigned for use on NYTimes.com (to get standardized terms, use the TimesTags API)
|
591
|
+
#
|
592
|
+
# When used in a request, values must be Mixed Case
|
593
|
+
# 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)
|
594
|
+
#
|
595
|
+
# When used in a request, values must be Mixed Case
|
596
|
+
# nytd_lead_paragraph String X X The first paragraph of the article (as it appears on NYTimes.com)
|
597
|
+
# 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)
|
598
|
+
#
|
599
|
+
# When used in a request, values must be Mixed Case
|
600
|
+
# 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)
|
601
|
+
#
|
602
|
+
# When used in a request, values must be Mixed Case
|
603
|
+
# nytd_section_facet Array (Strings) X X The section the article appears in (on NYTimes.com)
|
604
|
+
# 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)
|
605
|
+
# nytd_works_mentioned
|
606
|
+
# _facet String X X Literary works mentioned (titles formatted for use on NYTimes.com)
|
607
|
+
# org_facet Array (Strings) X X Standardized names of organizations, assigned by Times indexers
|
608
|
+
#
|
609
|
+
# When used in a request, values must be UPPERCASE
|
610
|
+
# page_facet String X X The page the article appeared on (in the printed paper)
|
611
|
+
# per_facet Array (Strings) X X Standardized names of people, assigned by Times indexers
|
612
|
+
#
|
613
|
+
# When used in a request, values must be UPPERCASE
|
614
|
+
# publication_day
|
615
|
+
# publication_month
|
616
|
+
# publication_year Date
|
617
|
+
# Date
|
618
|
+
# Date X
|
619
|
+
# X
|
620
|
+
# X X
|
621
|
+
# x
|
622
|
+
# x The day (DD), month (MM) and year (YYYY) segments of date, separated for use as facets
|
623
|
+
# 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.
|
624
|
+
# section_page_facet String X X The full page number of the printed article (e.g., D00002)
|
625
|
+
# small_image
|
626
|
+
# small_image_url
|
627
|
+
# small_image_height
|
628
|
+
# small_image_width Boolean
|
629
|
+
# String
|
630
|
+
# Integer
|
631
|
+
# Integer X X
|
632
|
+
# X
|
633
|
+
# X
|
634
|
+
# 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.
|
635
|
+
# source_facet String X X The originating body (e.g., AP, Dow Jones, The New York Times)
|
636
|
+
# 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.
|
637
|
+
# title String X X The article title (headline); corresponds to the headline that appeared in the printed newspaper
|
638
|
+
# url String X X The URL of the article on NYTimes.com
|
639
|
+
# word_count Integer X The full article word count
|
640
|
+
# works_mentioned_facet
|