data_output_agent_spec.rb 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. # encoding: utf-8
  2. require 'rails_helper'
  3. describe Agents::DataOutputAgent do
  4. let(:agent) do
  5. _agent = Agents::DataOutputAgent.new(:name => 'My Data Output Agent')
  6. _agent.options = _agent.default_options.merge('secrets' => ['secret1', 'secret2'], 'events_to_show' => 3)
  7. _agent.options['template']['item']['pubDate'] = "{{date}}"
  8. _agent.user = users(:bob)
  9. _agent.sources << agents(:bob_website_agent)
  10. _agent.save!
  11. _agent
  12. end
  13. describe "#working?" do
  14. it "checks if events have been received within expected receive period" do
  15. expect(agent).not_to be_working
  16. Agents::DataOutputAgent.async_receive agent.id, [events(:bob_website_agent_event).id]
  17. expect(agent.reload).to be_working
  18. two_days_from_now = 2.days.from_now
  19. stub(Time).now { two_days_from_now }
  20. expect(agent.reload).not_to be_working
  21. end
  22. end
  23. describe "validation" do
  24. before do
  25. expect(agent).to be_valid
  26. end
  27. it "should validate presence and length of secrets" do
  28. agent.options[:secrets] = ""
  29. expect(agent).not_to be_valid
  30. agent.options[:secrets] = "foo"
  31. expect(agent).not_to be_valid
  32. agent.options[:secrets] = "foo/bar"
  33. expect(agent).not_to be_valid
  34. agent.options[:secrets] = "foo.xml"
  35. expect(agent).not_to be_valid
  36. agent.options[:secrets] = false
  37. expect(agent).not_to be_valid
  38. agent.options[:secrets] = []
  39. expect(agent).not_to be_valid
  40. agent.options[:secrets] = ["foo.xml"]
  41. expect(agent).not_to be_valid
  42. agent.options[:secrets] = ["hello", true]
  43. expect(agent).not_to be_valid
  44. agent.options[:secrets] = ["hello"]
  45. expect(agent).to be_valid
  46. agent.options[:secrets] = ["hello", "world"]
  47. expect(agent).to be_valid
  48. end
  49. it "should validate presence of expected_receive_period_in_days" do
  50. agent.options[:expected_receive_period_in_days] = ""
  51. expect(agent).not_to be_valid
  52. agent.options[:expected_receive_period_in_days] = 0
  53. expect(agent).not_to be_valid
  54. agent.options[:expected_receive_period_in_days] = -1
  55. expect(agent).not_to be_valid
  56. end
  57. it "should validate presence of template and template.item" do
  58. agent.options[:template] = ""
  59. expect(agent).not_to be_valid
  60. agent.options[:template] = {}
  61. expect(agent).not_to be_valid
  62. agent.options[:template] = { 'item' => 'foo' }
  63. expect(agent).not_to be_valid
  64. agent.options[:template] = { 'item' => { 'title' => 'hi' } }
  65. expect(agent).to be_valid
  66. end
  67. end
  68. describe "#receive" do
  69. it "should push to hubs when push_hubs is given" do
  70. agent.options[:push_hubs] = %w[http://push.example.com]
  71. agent.options[:template] = { 'link' => 'http://huginn.example.org' }
  72. alist = nil
  73. stub_request(:post, 'http://push.example.com/')
  74. .with(headers: { 'Content-Type' => %r{\Aapplication/x-www-form-urlencoded\s*(?:;|\z)} })
  75. .to_return { |request|
  76. alist = URI.decode_www_form(request.body).sort
  77. { status: 200, body: 'ok' }
  78. }
  79. agent.receive(events(:bob_website_agent_event))
  80. expect(alist).to eq [
  81. ["hub.mode", "publish"],
  82. ["hub.url", agent.feed_url(secret: agent.options[:secrets].first, format: :xml)]
  83. ]
  84. end
  85. end
  86. describe "#receive_web_request" do
  87. before do
  88. current_time = Time.now
  89. stub(Time).now { current_time }
  90. agents(:bob_website_agent).events.destroy_all
  91. end
  92. it "requires a valid secret" do
  93. content, status, content_type = agent.receive_web_request({ 'secret' => 'fake' }, 'get', 'text/xml')
  94. expect(status).to eq(401)
  95. expect(content).to eq("Not Authorized")
  96. content, status, content_type = agent.receive_web_request({ 'secret' => 'fake' }, 'get', 'application/json')
  97. expect(status).to eq(401)
  98. expect(content).to eq({ :error => "Not Authorized" })
  99. content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'application/json')
  100. expect(status).to eq(200)
  101. end
  102. describe "outputting events as RSS and JSON" do
  103. let!(:event1) do
  104. agents(:bob_website_agent).create_event :payload => {
  105. "site_title" => "XKCD",
  106. "url" => "http://imgs.xkcd.com/comics/evolving.png",
  107. "title" => "Evolving",
  108. "hovertext" => "Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution."
  109. }
  110. end
  111. let!(:event2) do
  112. agents(:bob_website_agent).create_event :payload => {
  113. "site_title" => "XKCD",
  114. "url" => "http://imgs.xkcd.com/comics/evolving2.png",
  115. "title" => "Evolving again",
  116. "date" => '',
  117. "hovertext" => "Something else"
  118. }
  119. end
  120. let!(:event3) do
  121. agents(:bob_website_agent).create_event :payload => {
  122. "site_title" => "XKCD",
  123. "url" => "http://imgs.xkcd.com/comics/evolving0.png",
  124. "title" => "Evolving yet again with a past date",
  125. "date" => '2014/05/05',
  126. "hovertext" => "A small text"
  127. }
  128. end
  129. it "can output RSS" do
  130. stub(agent).feed_link { "https://yoursite.com" }
  131. content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
  132. expect(status).to eq(200)
  133. expect(content_type).to eq('text/xml')
  134. expect(content.gsub(/\s+/, '')).to eq Utils.unindent(<<-XML).gsub(/\s+/, '')
  135. <?xml version="1.0" encoding="UTF-8" ?>
  136. <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
  137. <channel>
  138. <atom:link href="https://yoursite.com/users/#{agent.user.id}/web_requests/#{agent.id}/secret1.xml" rel="self" type="application/rss+xml"/>
  139. <atom:icon>https://yoursite.com/favicon.ico</atom:icon>
  140. <title>XKCD comics as a feed</title>
  141. <description>This is a feed of recent XKCD comics, generated by Huginn</description>
  142. <link>https://yoursite.com</link>
  143. <lastBuildDate>#{Time.now.rfc2822}</lastBuildDate>
  144. <pubDate>#{Time.now.rfc2822}</pubDate>
  145. <ttl>60</ttl>
  146. <item>
  147. <title>Evolving yet again with a past date</title>
  148. <description>Secret hovertext: A small text</description>
  149. <link>http://imgs.xkcd.com/comics/evolving0.png</link>
  150. <pubDate>#{Time.zone.parse(event3.payload['date']).rfc2822}</pubDate>
  151. <guid isPermaLink="false">#{event3.id}</guid>
  152. </item>
  153. <item>
  154. <title>Evolving again</title>
  155. <description>Secret hovertext: Something else</description>
  156. <link>http://imgs.xkcd.com/comics/evolving2.png</link>
  157. <pubDate>#{event2.created_at.rfc2822}</pubDate>
  158. <guid isPermaLink="false">#{event2.id}</guid>
  159. </item>
  160. <item>
  161. <title>Evolving</title>
  162. <description>Secret hovertext: Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.</description>
  163. <link>http://imgs.xkcd.com/comics/evolving.png</link>
  164. <pubDate>#{event1.created_at.rfc2822}</pubDate>
  165. <guid isPermaLink="false">#{event1.id}</guid>
  166. </item>
  167. </channel>
  168. </rss>
  169. XML
  170. end
  171. it "can output RSS with hub links when push_hubs is specified" do
  172. stub(agent).feed_link { "https://yoursite.com" }
  173. agent.options[:push_hubs] = %w[https://pubsubhubbub.superfeedr.com/ https://pubsubhubbub.appspot.com/]
  174. content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
  175. expect(status).to eq(200)
  176. expect(content_type).to eq('text/xml')
  177. xml = Nokogiri::XML(content)
  178. expect(xml.xpath('/rss/channel/atom:link[@rel="hub"]/@href').map(&:text).sort).to eq agent.options[:push_hubs].sort
  179. end
  180. it "can output JSON" do
  181. agent.options['template']['item']['foo'] = "hi"
  182. content, status, content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
  183. expect(status).to eq(200)
  184. expect(content).to eq({
  185. 'title' => 'XKCD comics as a feed',
  186. 'description' => 'This is a feed of recent XKCD comics, generated by Huginn',
  187. 'pubDate' => Time.now,
  188. 'items' => [
  189. {
  190. 'title' => 'Evolving yet again with a past date',
  191. 'description' => 'Secret hovertext: A small text',
  192. 'link' => 'http://imgs.xkcd.com/comics/evolving0.png',
  193. 'guid' => {"contents" => event3.id, "isPermaLink" => "false"},
  194. 'pubDate' => Time.zone.parse(event3.payload['date']).rfc2822,
  195. 'foo' => 'hi'
  196. },
  197. {
  198. 'title' => 'Evolving again',
  199. 'description' => 'Secret hovertext: Something else',
  200. 'link' => 'http://imgs.xkcd.com/comics/evolving2.png',
  201. 'guid' => {"contents" => event2.id, "isPermaLink" => "false"},
  202. 'pubDate' => event2.created_at.rfc2822,
  203. 'foo' => 'hi'
  204. },
  205. {
  206. 'title' => 'Evolving',
  207. 'description' => 'Secret hovertext: Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.',
  208. 'link' => 'http://imgs.xkcd.com/comics/evolving.png',
  209. 'guid' => {"contents" => event1.id, "isPermaLink" => "false"},
  210. 'pubDate' => event1.created_at.rfc2822,
  211. 'foo' => 'hi'
  212. }
  213. ]
  214. })
  215. end
  216. context 'with more events' do
  217. let!(:event4) do
  218. agents(:bob_website_agent).create_event payload: {
  219. 'site_title' => 'XKCD',
  220. 'url' => 'http://imgs.xkcd.com/comics/comic1.png',
  221. 'title' => 'Comic 1',
  222. 'date' => '',
  223. 'hovertext' => 'Hovertext for Comic 1'
  224. }
  225. end
  226. let!(:event5) do
  227. agents(:bob_website_agent).create_event payload: {
  228. 'site_title' => 'XKCD',
  229. 'url' => 'http://imgs.xkcd.com/comics/comic2.png',
  230. 'title' => 'Comic 2',
  231. 'date' => '',
  232. 'hovertext' => 'Hovertext for Comic 2'
  233. }
  234. end
  235. let!(:event6) do
  236. agents(:bob_website_agent).create_event payload: {
  237. 'site_title' => 'XKCD',
  238. 'url' => 'http://imgs.xkcd.com/comics/comic3.png',
  239. 'title' => 'Comic 3',
  240. 'date' => '',
  241. 'hovertext' => 'Hovertext for Comic 3'
  242. }
  243. end
  244. describe 'limiting' do
  245. it 'can select the last `events_to_show` events' do
  246. agent.options['events_to_show'] = 2
  247. content, _status, _content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
  248. expect(content['items'].map {|i| i["title"] }).to eq(["Comic 3", "Comic 2"])
  249. end
  250. end
  251. end
  252. describe 'ordering' do
  253. before do
  254. agent.options['events_order'] = ['{{hovertext}}']
  255. agent.options['events_list_order'] = ['{{title}}']
  256. end
  257. it 'can reorder the last `events_to_show` events based on a Liquid expression' do
  258. agent.options['events_to_show'] = 2
  259. asc_content, _status, _content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
  260. expect(asc_content['items'].map {|i| i["title"] }).to eq(["Evolving", "Evolving again"])
  261. agent.options['events_to_show'] = 40
  262. asc_content, _status, _content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
  263. expect(asc_content['items'].map {|i| i["title"] }).to eq(["Evolving", "Evolving again", "Evolving yet again with a past date"])
  264. agent.options['events_list_order'] = [['{{title}}', 'string', true]]
  265. desc_content, _status, _content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
  266. expect(desc_content['items']).to eq(asc_content['items'].reverse)
  267. end
  268. end
  269. describe "interpolating \"events\"" do
  270. before do
  271. agent.options['template']['title'] = "XKCD comics as a feed{% if events.first.site_title %} ({{events.first.site_title}}){% endif %}"
  272. agent.save!
  273. end
  274. it "can output RSS" do
  275. stub(agent).feed_link { "https://yoursite.com" }
  276. content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
  277. expect(status).to eq(200)
  278. expect(content_type).to eq('text/xml')
  279. expect(Nokogiri(content).at('/rss/channel/title/text()').text).to eq('XKCD comics as a feed (XKCD)')
  280. end
  281. it "can output JSON" do
  282. content, status, content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
  283. expect(status).to eq(200)
  284. expect(content['title']).to eq('XKCD comics as a feed (XKCD)')
  285. end
  286. end
  287. describe "with a specified icon" do
  288. before do
  289. agent.options['template']['icon'] = 'https://somesite.com/icon.png'
  290. agent.save!
  291. end
  292. it "can output RSS" do
  293. stub(agent).feed_link { "https://yoursite.com" }
  294. content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
  295. expect(status).to eq(200)
  296. expect(content_type).to eq('text/xml')
  297. expect(Nokogiri(content).at('/rss/channel/atom:icon/text()').text).to eq('https://somesite.com/icon.png')
  298. end
  299. end
  300. describe "with media namespace not set" do
  301. before do
  302. agent.options['ns_media'] = nil
  303. agent.save!
  304. end
  305. it "can output RSS" do
  306. stub(agent).feed_link { "https://yoursite.com" }
  307. content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
  308. expect(status).to eq(200)
  309. expect(content_type).to eq('text/xml')
  310. doc = Nokogiri(content)
  311. namespaces = doc.collect_namespaces
  312. expect(namespaces).not_to include("xmlns:media")
  313. end
  314. end
  315. describe "with media namespace set true" do
  316. before do
  317. agent.options['ns_media'] = 'true'
  318. agent.save!
  319. end
  320. it "can output RSS" do
  321. stub(agent).feed_link { "https://yoursite.com" }
  322. content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
  323. expect(status).to eq(200)
  324. expect(content_type).to eq('text/xml')
  325. doc = Nokogiri(content)
  326. namespaces = doc.collect_namespaces
  327. expect(namespaces).to include(
  328. "xmlns:media" => 'http://search.yahoo.com/mrss/'
  329. )
  330. end
  331. end
  332. describe "with media namespace set false" do
  333. before do
  334. agent.options['ns_media'] = 'false'
  335. agent.save!
  336. end
  337. it "can output RSS" do
  338. stub(agent).feed_link { "https://yoursite.com" }
  339. content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
  340. expect(status).to eq(200)
  341. expect(content_type).to eq('text/xml')
  342. doc = Nokogiri(content)
  343. namespaces = doc.collect_namespaces
  344. expect(namespaces).not_to include("xmlns:media")
  345. end
  346. end
  347. describe "with itunes namespace not set" do
  348. before do
  349. agent.options['ns_itunes'] = nil
  350. agent.save!
  351. end
  352. it "can output RSS" do
  353. stub(agent).feed_link { "https://yoursite.com" }
  354. content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
  355. expect(status).to eq(200)
  356. expect(content_type).to eq('text/xml')
  357. doc = Nokogiri(content)
  358. namespaces = doc.collect_namespaces
  359. expect(namespaces).not_to include("xmlns:itunes")
  360. end
  361. end
  362. describe "with itunes namespace set true" do
  363. before do
  364. agent.options['ns_itunes'] = 'true'
  365. agent.save!
  366. end
  367. it "can output RSS" do
  368. stub(agent).feed_link { "https://yoursite.com" }
  369. content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
  370. expect(status).to eq(200)
  371. expect(content_type).to eq('text/xml')
  372. doc = Nokogiri(content)
  373. namespaces = doc.collect_namespaces
  374. expect(namespaces).to include(
  375. "xmlns:itunes" => 'http://www.itunes.com/dtds/podcast-1.0.dtd'
  376. )
  377. end
  378. end
  379. describe "with itunes namespace set false" do
  380. before do
  381. agent.options['ns_itunes'] = 'false'
  382. agent.save!
  383. end
  384. it "can output RSS" do
  385. stub(agent).feed_link { "https://yoursite.com" }
  386. content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
  387. expect(status).to eq(200)
  388. expect(content_type).to eq('text/xml')
  389. doc = Nokogiri(content)
  390. namespaces = doc.collect_namespaces
  391. expect(namespaces).not_to include("xmlns:itunes")
  392. end
  393. end
  394. end
  395. describe "outputting nesting" do
  396. before do
  397. agent.options['template']['item']['enclosure'] = {
  398. "_attributes" => {
  399. "type" => "audio/mpeg",
  400. "url" => "{{media_url}}"
  401. }
  402. }
  403. agent.options['template']['item']['foo'] = {
  404. "_attributes" => {
  405. "attr" => "attr-value-{{foo}}"
  406. },
  407. "_contents" => "Foo: {{foo}}"
  408. }
  409. agent.options['template']['item']['nested'] = {
  410. "_attributes" => {
  411. "key" => "value"
  412. },
  413. "_contents" => {
  414. "title" => "some title"
  415. }
  416. }
  417. agent.options['template']['item']['simpleNested'] = {
  418. "title" => "some title",
  419. "complex" => {
  420. "_attributes" => {
  421. "key" => "value"
  422. },
  423. "_contents" => {
  424. "first" => {
  425. "_attributes" => {
  426. "a" => "b"
  427. },
  428. "_contents" => {
  429. "second" => "value"
  430. }
  431. }
  432. }
  433. }
  434. }
  435. agent.save!
  436. end
  437. let!(:event) do
  438. agents(:bob_website_agent).create_event :payload => {
  439. "url" => "http://imgs.xkcd.com/comics/evolving.png",
  440. "title" => "Evolving",
  441. "hovertext" => "Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.",
  442. "media_url" => "http://google.com/audio.mpeg",
  443. "foo" => 1
  444. }
  445. end
  446. it "can output JSON" do
  447. content, status, content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
  448. expect(status).to eq(200)
  449. expect(content['items'].first).to eq(
  450. {
  451. 'title' => 'Evolving',
  452. 'description' => 'Secret hovertext: Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.',
  453. 'link' => 'http://imgs.xkcd.com/comics/evolving.png',
  454. 'guid' => {"contents" => event.id, "isPermaLink" => "false"},
  455. 'pubDate' => event.created_at.rfc2822,
  456. 'enclosure' => {
  457. "type" => "audio/mpeg",
  458. "url" => "http://google.com/audio.mpeg"
  459. },
  460. 'foo' => {
  461. 'attr' => 'attr-value-1',
  462. 'contents' => 'Foo: 1'
  463. },
  464. 'nested' => {
  465. "key" => "value",
  466. "title" => "some title"
  467. },
  468. 'simpleNested' => {
  469. "title" => "some title",
  470. "complex" => {
  471. "key"=>"value",
  472. "first" => {
  473. "a" => "b",
  474. "second"=>"value"
  475. }
  476. }
  477. }
  478. }
  479. )
  480. end
  481. it "can output RSS" do
  482. stub(agent).feed_link { "https://yoursite.com" }
  483. content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
  484. expect(status).to eq(200)
  485. expect(content_type).to eq('text/xml')
  486. expect(content.gsub(/\s+/, '')).to eq Utils.unindent(<<-XML).gsub(/\s+/, '')
  487. <?xml version="1.0" encoding="UTF-8" ?>
  488. <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" >
  489. <channel>
  490. <atom:link href="https://yoursite.com/users/#{agent.user.id}/web_requests/#{agent.id}/secret1.xml" rel="self" type="application/rss+xml"/>
  491. <atom:icon>https://yoursite.com/favicon.ico</atom:icon>
  492. <title>XKCD comics as a feed</title>
  493. <description>This is a feed of recent XKCD comics, generated by Huginn</description>
  494. <link>https://yoursite.com</link>
  495. <lastBuildDate>#{Time.now.rfc2822}</lastBuildDate>
  496. <pubDate>#{Time.now.rfc2822}</pubDate>
  497. <ttl>60</ttl>
  498. <item>
  499. <title>Evolving</title>
  500. <description>Secret hovertext: Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.</description>
  501. <link>http://imgs.xkcd.com/comics/evolving.png</link>
  502. <pubDate>#{event.created_at.rfc2822}</pubDate>
  503. <enclosure type="audio/mpeg" url="http://google.com/audio.mpeg" />
  504. <foo attr="attr-value-1">Foo: 1</foo>
  505. <nested key="value"><title>some title</title></nested>
  506. <simpleNested>
  507. <title>some title</title>
  508. <complex key="value">
  509. <first a="b">
  510. <second>value</second>
  511. </first>
  512. </complex>
  513. </simpleNested>
  514. <guid isPermaLink="false">#{event.id}</guid>
  515. </item>
  516. </channel>
  517. </rss>
  518. XML
  519. end
  520. end
  521. end
  522. end