ess 0.9.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,8 @@
1
1
  module ESS
2
2
  module Examples
3
+ ##
4
+ # Example feeds for documentation and testing.
5
+ #
3
6
 
4
7
  def self.from_essfeeds_org_home
5
8
  ess = Maker.make do |ess|
@@ -45,7 +48,7 @@ module ESS
45
48
  item.selected_day_attr "saturday,sunday"
46
49
 
47
50
  item.name "Match Date"
48
- item.start "2011-12-13T18:30:02Z"
51
+ item.start "2011-12-17T18:30:02Z"
49
52
  item.duration "10800"
50
53
  end
51
54
 
@@ -129,7 +132,7 @@ module ESS
129
132
  item.country_code "US"
130
133
  item.logo "http://sample.com/logo_120x60.png"
131
134
  item.icon "http://sample.com/icon_64x64.png"
132
- item.email "contact@sample.com"
135
+ item.email "contact@example.com"
133
136
  item.phone "+001 (646) 234-5566"
134
137
  end
135
138
 
@@ -261,7 +264,7 @@ module ESS
261
264
 
262
265
  feed.dates.add_item :type => "recurrent", :unit => "month", :limit => 6, :selected_day => "saturday", :selected_week => "first,last" do |item|
263
266
  item.name "Course the first and last Saturdays of every month"
264
- item.start "2013-10-25T15:30:00Z"
267
+ item.start "2013-10-05T15:30:00Z"
265
268
  item.duration "21600"
266
269
  end
267
270
 
@@ -3,6 +3,9 @@ require 'digest/md5'
3
3
 
4
4
  module ESS
5
5
  module Helpers
6
+ ##
7
+ # Generates an UUID with the prefix specified, using the kay passed to it.
8
+ #
6
9
  def self.uuid key=nil, prefix='ESSID:'
7
10
  new_id = nil
8
11
  if key.nil?
@@ -1,12 +1,37 @@
1
1
  module ESS
2
2
  class Maker
3
- DEFAULT_OPTIONS = {
4
- :version => "0.9",
5
- :lang => "en",
6
- :validate => true,
7
- :push => false
8
- }
9
3
 
4
+ ##
5
+ # Create a new ESS document. See README for examples on how it should
6
+ # be used.
7
+ #
8
+ # === Yields
9
+ #
10
+ # [ESS::ESS] object representing the "ess" root tag of an ESS document
11
+ #
12
+ # === Options
13
+ #
14
+ # Currently, the following options are defined:
15
+ #
16
+ # ==== :push
17
+ #
18
+ # Whether to push the resulting document to aggregators before returning
19
+ # from the method. Default is false.
20
+ #
21
+ # ==== :validate
22
+ #
23
+ # Validate resulting document before returning from the method. Default
24
+ # is true.
25
+ #
26
+ # ==== :version
27
+ #
28
+ # Set a different value for the "version" attribute of the "ess" tag.
29
+ # Default is "0.9".
30
+ #
31
+ # ==== :lang
32
+ #
33
+ # Set a value for the "lang" attribute of the "ess" tag. Default is "en".
34
+ #
10
35
  def self.make options={}, &block
11
36
  options = DEFAULT_OPTIONS.merge options
12
37
  ess = ESS.new
@@ -19,6 +44,15 @@ module ESS
19
44
  ess.push_to_aggregators(options) if options[:push] || options[:aggregators]
20
45
  return ess
21
46
  end
47
+
48
+ private
49
+
50
+ DEFAULT_OPTIONS = {
51
+ :version => "0.9",
52
+ :lang => "en",
53
+ :validate => true,
54
+ :push => false
55
+ }
22
56
  end
23
57
  end
24
58
 
@@ -0,0 +1,31 @@
1
+ require "ess"
2
+ require "rexml/document"
3
+
4
+ module ESS
5
+ class Parser
6
+ def self.parse data
7
+ doc = REXML::Document.new data
8
+ ess = doc.root
9
+ if ess.nil?
10
+ raise ArgumentError, "the argument has to contain a valid xml document"
11
+ end
12
+ new_ess = ESS.new
13
+ new_ess.disable_postprocessing
14
+ parse_element(ess, new_ess)
15
+ new_ess.enable_postprocessing
16
+ new_ess
17
+ end
18
+
19
+ def self.parse_element element, new_element=nil
20
+ element.attributes.each_pair do |attr, value|
21
+ new_element.send(attr + "_attr", value)
22
+ end
23
+ element.elements.each do |element|
24
+ new_element.send("add_" + element.name, element.text) do |new_element|
25
+ parse_element element, new_element
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
@@ -4,6 +4,10 @@ require 'date'
4
4
 
5
5
  module ESS
6
6
  module Postprocessing
7
+ ##
8
+ # Varous classes for postprocessing tag values. Should not be used directly,
9
+ # instances of it's classes are part of the DTD.
10
+ #
7
11
  class FeedTitle
8
12
  def process feed_tag, title_tag
9
13
  feed_tag.id(title_tag.text!) if feed_tag.id.text! == ""
@@ -5,15 +5,53 @@ require 'uri'
5
5
 
6
6
  module ESS
7
7
  module Pusher
8
+ ##
9
+ # Sets the default aggregator services. Accepts a list of links in strings.
10
+ #
8
11
  def self.aggregators= aggs
9
12
  raise ArgumentError, "this method requires a list of links" if aggs.class != Array
10
13
  @@aggregators = aggs
11
14
  end
12
15
 
16
+ ##
17
+ # Returns the aggregator services currently set as default.
18
+ #
13
19
  def self.aggregators
14
20
  @@aggregators ||= ["http://api.hypecal.com/v1/ess/aggregator.json"]
15
21
  end
16
22
 
23
+ ##
24
+ # Pushes the feed to aggregators.
25
+ #
26
+ # === Options
27
+ #
28
+ # ==== :data
29
+ #
30
+ # A string, with an XML document representing the feed that needs to be
31
+ # pushed.
32
+ #
33
+ # ==== :feed
34
+ #
35
+ # This is an alternative to the :data option. This method accepts the feed
36
+ # link instead of the whole document. The aggregator service will pull the
37
+ # feed using this link.
38
+ #
39
+ # ==== :request
40
+ #
41
+ # This should be the request object that generated this feed. It can be
42
+ # useful for the aggregator service to receive some of th information
43
+ # from this object for crawling purposes.
44
+ #
45
+ # ==== :aggregators
46
+ #
47
+ # Intead of using the default aggregator services, this options can be used
48
+ # to specify what aggregators should the document be pushed to.
49
+ #
50
+ # ==== :ignore_errors
51
+ #
52
+ # The method will ignore any response from the aggregator services if this
53
+ # option is true. Default is false.
54
+ #
17
55
  def self.push_to_aggregators options={}
18
56
  options = { :aggregators => Pusher::aggregators,
19
57
  :feed => nil,
@@ -1,10 +1,20 @@
1
1
  require 'ess/helpers'
2
2
  require 'time'
3
+ require 'resolv'
3
4
 
4
5
  module ESS
5
6
  module Validation
7
+ ##
8
+ # Varous classes for validation of tag values. Should not be used directly,
9
+ # instances of it's classes are part of the DTD.
10
+ #
6
11
 
7
12
  class ValidationError < StandardError
13
+ ##
14
+ # This exception is generated when the validator find an error
15
+ # in the document. It should contain a message describing what
16
+ # was the value which caused it to be raised.
17
+ #
8
18
  end
9
19
 
10
20
  class TextIsNotNull
@@ -17,7 +27,32 @@ module ESS
17
27
 
18
28
  class TextIsValidEmail
19
29
  def validate tag
20
- # TODO: Doing this is not recommended, check if realy necessary
30
+ return if valid_domain?(tag.text!)
31
+ raise InvalidValueError, "the email \"#{tag.text!}\" could not be validated"
32
+ end
33
+
34
+ EMAIL_PATTERN = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
35
+ SERVER_TIMEOUT = 10
36
+
37
+ def valid_domain?(email)
38
+ domain = email.match(EMAIL_PATTERN)[2]
39
+ dns = Resolv::DNS.new
40
+ Timeout::timeout(SERVER_TIMEOUT) do
41
+
42
+ # Check the MX record
43
+ mx_records = dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
44
+
45
+ mx_records.sort_by {|mx| mx.preference}.each do |mx|
46
+ a_records = dns.getresources(mx.exchange.to_s, Resolv::DNS::Resource::IN::A)
47
+ return true if a_records.any?
48
+ end
49
+
50
+ #Try a straight A record
51
+ a_records = dns.getresources(domain, Resolv::DNS::Resource::IN::A)
52
+ a_records.any?
53
+ end
54
+ rescue Timeout::Error, Errno::ECONNREFUSED
55
+ false
21
56
  end
22
57
  end
23
58
 
@@ -1,3 +1,3 @@
1
1
  module ESS
2
- VERSION = "0.9.3"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -0,0 +1,350 @@
1
+ require 'spec_helper'
2
+
3
+ module ESS
4
+ describe "ESS" do
5
+ let(:future_year) { Time.now.year + 2 }
6
+
7
+ context 'feed with one item with standalone date' do
8
+ let(:ess) do
9
+ ess = ESS.new
10
+ ess.channel.add_feed do |feed|
11
+ feed.title "Feed 1"
12
+ feed.dates.add_item do |item|
13
+ item.type_attr "standalone"
14
+ item.name "Date 1"
15
+ item.start Time.parse("#{future_year}-01-01T00:00:00Z")
16
+ end
17
+ end
18
+ ess
19
+ end
20
+
21
+ describe '#find_coming' do
22
+ it 'should return that one item in an array' do
23
+ feeds = ess.find_coming
24
+ feeds.length.should == 1
25
+ feeds[0][:feed].title.text!.should == "Feed 1"
26
+ end
27
+ end
28
+
29
+ describe '#find_between' do
30
+ it 'should return that one item in an array if it\'s between the two moments in time' do
31
+ feeds = ess.find_between(Time.parse("#{future_year-1}-12-31T23:59:00Z"), Time.parse("#{future_year}-01-01T00:01:00Z"))
32
+ feeds.length.should == 1
33
+ feeds[0][:feed].title.text!.should == "Feed 1"
34
+ end
35
+
36
+ it 'should return an empty array if the item event is not between the two moments in time' do
37
+ feeds = ess.find_between(Time.parse("#{future_year}-01-01T00:01:00Z"), Time.parse("#{future_year}-01-01T00:03:00Z"))
38
+ feeds.length.should == 0
39
+ end
40
+ end
41
+ end
42
+
43
+ context 'ESS channel with two feeds with future standalone dates' do
44
+ let(:ess) do
45
+ ess = ESS.new
46
+ ess.channel.add_feed do |feed|
47
+ feed.title "Feed 1"
48
+ feed.dates.add_item do |item|
49
+ item.type_attr "standalone"
50
+ item.name "Date 1"
51
+ item.start Time.parse("#{future_year}-01-01T00:00:00Z")
52
+ end
53
+ end
54
+ ess.channel.add_feed do |feed|
55
+ feed.title "Feed 2"
56
+ feed.dates.add_item do |item|
57
+ item.type_attr "standalone"
58
+ item.name "Date 1"
59
+ item.start Time.parse("#{future_year}-01-08T00:00:00Z")
60
+ end
61
+ end
62
+ ess
63
+ end
64
+
65
+ describe '#find_coming' do
66
+ it 'should return both items in an array if no parameters were specified' do
67
+ feeds = ess.find_coming
68
+ feeds.length.should == 2
69
+ feeds[0][:feed].title.text!.should == "Feed 1"
70
+ feeds[1][:feed].title.text!.should == "Feed 2"
71
+ end
72
+
73
+ it 'should return the first feed in an array if passed the number 1' do
74
+ feeds = ess.find_coming(1)
75
+ feeds.length.should == 1
76
+ feeds[0][:feed].title.text!.should == "Feed 1"
77
+ end
78
+
79
+ it 'should return both items in an array if passed the number 2' do
80
+ feeds = ess.find_coming(2)
81
+ feeds.length.should == 2
82
+ feeds[0][:feed].title.text!.should == "Feed 1"
83
+ feeds[1][:feed].title.text!.should == "Feed 2"
84
+ end
85
+
86
+ it 'should return both items in an array if passed the number 2, sorted by asc. date/time' do
87
+ feeds = ess.channel.feed_list
88
+ tmp1 = feeds[0]
89
+ tmp2 = feeds[1]
90
+ ess.channel.feed_list.clear
91
+ ess.channel.feed_list << tmp2
92
+ ess.channel.feed_list << tmp1
93
+ feeds = ess.find_coming(2)
94
+ feeds.length.should == 2
95
+ feeds[0][:feed].title.text!.should == "Feed 1"
96
+ feeds[1][:feed].title.text!.should == "Feed 2"
97
+ end
98
+ it 'should return both items in an array if passed the number 3' do
99
+ feeds = ess.find_coming(3)
100
+ feeds.length.should == 2
101
+ feeds[0][:feed].title.text!.should == "Feed 1"
102
+ feeds[1][:feed].title.text!.should == "Feed 2"
103
+ end
104
+ end
105
+
106
+ describe '#find_between' do
107
+ it 'should return an empty list if none of the events is between two moments in time' do
108
+ feeds = ess.find_between(Time.parse("#{future_year-1}-12-21T23:59:00Z"), Time.parse("#{future_year-1}-12-31T00:01:00Z"))
109
+ feeds.length.should == 0
110
+ feeds = ess.find_between(Time.parse("#{future_year}-01-10T23:59:00Z"), Time.parse("#{future_year}-01-13T00:01:00Z"))
111
+ feeds.length.should == 0
112
+ end
113
+
114
+ it 'should return the first feed if it is happening between the two dates' do
115
+ feeds = ess.find_between(Time.parse("#{future_year-1}-12-21T23:59:00Z"), Time.parse("#{future_year}-01-05T00:01:00Z"))
116
+ feeds.length.should == 1
117
+ feeds[0][:feed].title.text!.should == "Feed 1"
118
+ end
119
+
120
+ it 'should return the second feed if it is happening between the two dates' do
121
+ feeds = ess.find_between(Time.parse("#{future_year}-01-01T00:02:00Z"), Time.parse("#{future_year}-01-09T00:01:00Z"))
122
+ feeds.length.should == 1
123
+ feeds[0][:feed].title.text!.should == "Feed 2"
124
+ end
125
+
126
+ it 'should return both items if they both happen in the interval' do
127
+ feeds = ess.find_between(Time.parse("#{future_year}-01-01T00:00:00Z"), Time.parse("#{future_year}-01-09T00:01:00Z"))
128
+ feeds.length.should == 2
129
+ feeds[0][:feed].title.text!.should == "Feed 1"
130
+ feeds[1][:feed].title.text!.should == "Feed 2"
131
+ end
132
+ end
133
+ end
134
+
135
+ context 'one feed with two date items with standalone values' do
136
+ let(:ess) do
137
+ ess = ESS.new
138
+ ess.channel.add_feed do |feed|
139
+ feed.title "Feed 1"
140
+ feed.dates.add_item do |item|
141
+ item.type_attr "standalone"
142
+ item.name "Date 2"
143
+ item.start Time.parse("#{future_year}-01-01T00:15:00Z")
144
+ end
145
+ feed.dates.add_item do |item|
146
+ item.type_attr "standalone"
147
+ item.name "Date 1"
148
+ item.start Time.parse("#{future_year}-01-01T00:00:00Z")
149
+ end
150
+ end
151
+ ess
152
+ end
153
+
154
+ describe "#find_coming" do
155
+ it 'should return two items in a list, if no arguments are passed' do
156
+ ess.find_coming.length.should == 2
157
+ end
158
+
159
+ it 'should return one item in a list, if the only argument is a 1' do
160
+ ess.find_coming(1).length.should == 1
161
+ end
162
+
163
+ it 'should return a list of dicts, with keys :time and :feed' do
164
+ feeds = ess.find_coming
165
+ feeds.each do |feed|
166
+ feed[:time].should_not be_nil
167
+ feed[:feed].should_not be_nil
168
+ end
169
+ end
170
+
171
+ it 'should return a list of discts sorted by ascending order of time' do
172
+ feeds = ess.find_coming
173
+ feeds[0][:time].should == Time.parse("#{future_year}-01-01T00:00:00Z")
174
+ feeds[0][:feed].title.text!.should == "Feed 1"
175
+ feeds[1][:time].should == Time.parse("#{future_year}-01-01T00:15:00Z")
176
+ feeds[1][:feed].title.text!.should == "Feed 1"
177
+ end
178
+ end
179
+
180
+ describe "#find_between" do
181
+ it 'should return two items in a list, if both date items are within the time period' do
182
+ feeds = ess.find_between(Time.parse("#{future_year}-01-01T00:00:00Z"), Time.parse("#{future_year}-01-09T00:01:00Z"))
183
+ feeds.length == 2
184
+ end
185
+
186
+ it 'should return two hashes in a list, with :time and :feed values defined' do
187
+ feeds = ess.find_between(Time.parse("#{future_year}-01-01T00:00:00Z"), Time.parse("#{future_year}-01-09T00:01:00Z"))
188
+ feeds.each do |feed|
189
+ feed[:time].should_not be_nil
190
+ feed[:feed].should_not be_nil
191
+ end
192
+ end
193
+
194
+ it 'should return a list of dicts sorted by ascending order of time' do
195
+ feeds = ess.find_coming
196
+ feeds[0][:time].should == Time.parse("#{future_year}-01-01T00:00:00Z")
197
+ feeds[0][:feed].title.text!.should == "Feed 1"
198
+ feeds[1][:time].should == Time.parse("#{future_year}-01-01T00:15:00Z")
199
+ feeds[1][:feed].title.text!.should == "Feed 1"
200
+ end
201
+ end
202
+ end
203
+
204
+ context 'feed with one item with recurring date with a limit attribute' do
205
+ let(:ess) do
206
+ ess = ESS.new
207
+ ess.channel.add_feed do |feed|
208
+ feed.title "Feed 1"
209
+ feed.dates.add_item do |item|
210
+ item.type_attr "recurrent"
211
+ item.unit_attr "month"
212
+ item.limit_attr "5"
213
+ item.name "Date 1"
214
+ item.start Time.parse("#{future_year}-01-01T00:00:00Z")
215
+ end
216
+ end
217
+ ess
218
+ end
219
+
220
+ describe "#find_coming" do
221
+ it 'should return the array with as much elements, as there will be separate events' do
222
+ ess.find_coming.length.should == 5
223
+ end
224
+
225
+ it 'should return a list of feeds, sorted by starting date/time' do
226
+ feeds = ess.find_coming
227
+ feeds[0][:time].should == Time.parse("#{future_year}-01-01T00:00:00Z")
228
+ feeds[1][:time].should == Time.parse("#{future_year}-02-01T00:00:00Z")
229
+ feeds[2][:time].should == Time.parse("#{future_year}-03-01T00:00:00Z")
230
+ feeds[3][:time].should == Time.parse("#{future_year}-04-01T00:00:00Z")
231
+ feeds[4][:time].should == Time.parse("#{future_year}-05-01T00:00:00Z")
232
+ end
233
+ end
234
+
235
+ describe "#find_coming" do
236
+ it 'should return a list of event instances within time boundaries' do
237
+ feeds = ess.find_between(Time.parse("#{future_year}-02-11T00:00:00Z"), Time.parse("#{future_year}-07-11T12:00:00Z"))
238
+ feeds.length.should == 3
239
+ feeds[0][:time].should == Time.parse("#{future_year}-03-01T00:00:00Z")
240
+ feeds[1][:time].should == Time.parse("#{future_year}-04-01T00:00:00Z")
241
+ feeds[2][:time].should == Time.parse("#{future_year}-05-01T00:00:00Z")
242
+ end
243
+ end
244
+ end
245
+
246
+ context 'feed with one item with recurring date with a limit attribute and an interval attribute' do
247
+ let(:ess) do
248
+ ess = ESS.new
249
+ ess.channel.add_feed do |feed|
250
+ feed.title "Feed 1"
251
+ feed.dates.add_item do |item|
252
+ item.type_attr "recurrent"
253
+ item.unit_attr "month"
254
+ item.interval_attr "2"
255
+ item.limit_attr "5"
256
+ item.name "Date 1"
257
+ item.start Time.parse("#{future_year}-01-01T00:00:00Z")
258
+ end
259
+ end
260
+ ess
261
+ end
262
+
263
+ describe "#find_coming" do
264
+ it 'should return the array with as much elements, as there will be separate events' do
265
+ ess.find_coming.length.should == 5
266
+ end
267
+
268
+ it 'should return a list of feeds, sorted by starting date/time' do
269
+ feeds = ess.find_coming
270
+ feeds[0][:time].should == Time.parse("#{future_year}-01-01T00:00:00Z")
271
+ feeds[1][:time].should == Time.parse("#{future_year}-03-01T00:00:00Z")
272
+ feeds[2][:time].should == Time.parse("#{future_year}-05-01T00:00:00Z")
273
+ feeds[3][:time].should == Time.parse("#{future_year}-07-01T00:00:00Z")
274
+ feeds[4][:time].should == Time.parse("#{future_year}-09-01T00:00:00Z")
275
+ end
276
+ end
277
+
278
+ describe "#find_between" do
279
+ it 'should return a list of feeds between the two dates it received as parameters' do
280
+ feeds = ess.find_between(Time.parse("#{future_year}-03-11T00:00:00Z"), Time.parse("#{future_year}-10-11T00:00:00Z"))
281
+ feeds.length.should == 3
282
+ feeds[0][:time].should == Time.parse("#{future_year}-05-01T00:00:00Z")
283
+ feeds[1][:time].should == Time.parse("#{future_year}-07-01T00:00:00Z")
284
+ feeds[2][:time].should == Time.parse("#{future_year}-09-01T00:00:00Z")
285
+ end
286
+ end
287
+ end
288
+
289
+ context 'feed with one item with recurring date with a limit attribute and a selected_day attribute' do
290
+ let(:ess) do
291
+ ess = ESS.new
292
+ ess.channel.add_feed do |feed|
293
+ feed.title "Feed 1"
294
+ feed.dates.add_item do |item|
295
+ item.type_attr "recurrent"
296
+ item.unit_attr "month"
297
+ item.limit_attr "5"
298
+ item.selected_day_attr "2"
299
+ item.name "Date 1"
300
+ item.start Time.parse("#{future_year}-01-02T00:00:00Z")
301
+ end
302
+ end
303
+ ess
304
+ end
305
+
306
+ describe "#find_coming" do
307
+ it 'should return events only on those days specified in the selected_day attribute' do
308
+ feeds = ess.find_coming
309
+ feeds.length.should == 5
310
+ feeds[0][:time].should == Time.parse("#{future_year}-01-02T00:00:00Z")
311
+ feeds[1][:time].should == Time.parse("#{future_year}-02-02T00:00:00Z")
312
+ feeds[2][:time].should == Time.parse("#{future_year}-03-02T00:00:00Z")
313
+ feeds[3][:time].should == Time.parse("#{future_year}-04-02T00:00:00Z")
314
+ feeds[4][:time].should == Time.parse("#{future_year}-05-02T00:00:00Z")
315
+ end
316
+
317
+ it 'should support using day names' do
318
+ ess.channel.feed.dates.item.selected_day_attr "monday"
319
+ feeds = ess.find_coming(30)
320
+ feeds.length.should be > 19
321
+ feeds.length.should be < 26
322
+ feeds.each do |event|
323
+ event[:time].wday.should == 1
324
+ end
325
+ end
326
+ end
327
+
328
+ describe "#find_between" do
329
+ it 'should return events only on those days specified in the selected_day attribute' do
330
+ feeds = ess.find_between(Time.parse("#{future_year}-01-05 00:00"), Time.parse("#{future_year}-04-06T00:00:00Z"))
331
+ feeds.length.should == 3
332
+ feeds[0][:time].should == Time.parse("#{future_year}-02-02T00:00:00Z")
333
+ feeds[1][:time].should == Time.parse("#{future_year}-03-02T00:00:00Z")
334
+ feeds[2][:time].should == Time.parse("#{future_year}-04-02T00:00:00Z")
335
+ end
336
+
337
+ it 'should support using day names' do
338
+ ess.channel.feed.dates.item.selected_day_attr "monday"
339
+ feeds = ess.find_between(Time.parse("#{future_year}-02-01T00:00:00Z"), Time.parse("#{future_year}-04-30T00:00:00Z"))
340
+ feeds.length.should be > 11
341
+ feeds.length.should be < 16
342
+ feeds.each do |event|
343
+ event[:time].wday.should == 1
344
+ end
345
+ end
346
+ end
347
+ end
348
+ end
349
+ end
350
+