From 8855a0bfbee7e9a81b8f66c4df22d6cff69e68ae Mon Sep 17 00:00:00 2001
From: Alexander <alxndr+github@gmail.com>
Date: Thu, 21 Feb 2013 10:20:00 -0800
Subject: [PATCH] RSS of a topic via new route Adds TopicView#recent_posts;
 Post#by_newest, #with_user, #author_readable; User#readable_name
 Autodiscovery tag in topic show HTML.

---
 app/controllers/topics_controller.rb       |  7 ++++++-
 app/models/post.rb                         |  7 +++++++
 app/models/user.rb                         |  8 ++++++++
 app/views/layouts/application.html.erb     |  2 ++
 app/views/topics/show.html.erb             |  4 ++++
 app/views/topics/show.rss.erb              | 22 ++++++++++++++++++++
 config/routes.rb                           |  1 +
 lib/topic_view.rb                          |  4 ++++
 spec/components/topic_view_spec.rb         | 19 +++++++++++++++++
 spec/controllers/topics_controller_spec.rb | 10 +++++++++
 spec/models/post_spec.rb                   | 24 ++++++++++++++++++++++
 spec/models/user_spec.rb                   | 18 ++++++++++++++++
 12 files changed, 125 insertions(+), 1 deletion(-)
 create mode 100644 app/views/topics/show.rss.erb

diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb
index 682e52b5a..80e8a8cce 100644
--- a/app/controllers/topics_controller.rb
+++ b/app/controllers/topics_controller.rb
@@ -17,7 +17,7 @@ class TopicsController < ApplicationController
                                           :move_posts]
   before_filter :consider_user_for_promotion, only: :show
 
-  skip_before_filter :check_xhr, only: [:avatar, :show]
+  skip_before_filter :check_xhr, only: [:avatar, :show, :feed]
   caches_action :avatar, :cache_path => Proc.new {|c| "#{c.params[:post_number]}-#{c.params[:topic_id]}" }
 
 
@@ -141,6 +141,11 @@ class TopicsController < ApplicationController
     render nothing: true
   end
 
+  def feed
+    @topic_view = TopicView.new(params[:topic_id])
+    render 'topics/show', formats: [:rss]
+  end
+
   private
 
   def create_topic_view
diff --git a/app/models/post.rb b/app/models/post.rb
index 4a5ee0b23..4f5e4007f 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -57,6 +57,9 @@ class Post < ActiveRecord::Base
     TopicUser.auto_track(self.user_id, self.topic_id, TopicUser::NotificationReasons::CREATED_POST)
   end
 
+  scope :by_newest, order('created_at desc, id desc')
+  scope :with_user, includes(:user)
+
   def raw_quality
 
     sentinel = TextSentinel.new(self.raw, min_entropy: SiteSetting.body_min_entropy)
@@ -298,6 +301,10 @@ class Post < ActiveRecord::Base
     "/t/#{Slug.for(topic.title)}/#{topic.id}/#{post_number}"
   end
 
+  def author_readable
+    user.readable_name
+  end
+
   def revise(updated_by, new_raw, opts={})
     PostRevisor.new(self).revise!(updated_by, new_raw, opts)
   end
diff --git a/app/models/user.rb b/app/models/user.rb
index 9626cc338..564aa480f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -458,6 +458,14 @@ class User < ActiveRecord::Base
     $redis.set(last_seen_key, Time.now.to_f)
   end
 
+  def readable_name
+    if name.present? && name != username
+      "#{name} (#{username})"
+    else
+      username
+    end
+  end
+
   protected
 
     def cook
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 923766581..e1b44ca5b 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -19,6 +19,8 @@
 
 
     <%=csrf_meta_tags%>
+
+    <%= yield :head %>
   </head>
 
   <body>
diff --git a/app/views/topics/show.html.erb b/app/views/topics/show.html.erb
index c61b80621..2c669554f 100644
--- a/app/views/topics/show.html.erb
+++ b/app/views/topics/show.html.erb
@@ -21,3 +21,7 @@
 
 
 <p>Powered by <a href="http://www.discourse.org">Discourse</a>, best viewed with JavaScript enabled</p>
+
+<% content_for :head do %>
+  <%= auto_discovery_link_tag(@topic_view, {action: :feed, format: :rss}, title: "RSS feed of '#{@topic_view.title}'") %>
+<% end %>
diff --git a/app/views/topics/show.rss.erb b/app/views/topics/show.rss.erb
new file mode 100644
index 000000000..3485c8a26
--- /dev/null
+++ b/app/views/topics/show.rss.erb
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
+  <channel>
+    <title><%= @topic_view.title %></title>
+    <link><%= Discourse.base_url %><%= @topic_view.relative_url %></link>
+    <description><%= @topic_view.posts.first.raw %></description>
+    <atom:link href="<%= Discourse.base_url %><%= @topic_view.relative_url %>.rss" rel="self" type="application/rss+xml" />
+    <% @topic_view.recent_posts.each do |post| %>
+      <item>
+        <title><%= @topic_view.title %> at <%= post.created_at %></title>
+        <description><![CDATA[
+          <p><%= post.author_readable %> wrote:</p>
+          <%= post.cooked.html_safe %>
+        ]]></description>
+        <link><%= Discourse.base_url %><%= post.url %></link>
+        <pubDate><%= post.created_at.rfc2822 %></pubDate>
+        <guid><%= Discourse.base_url %><%= post.url %></guid>
+        <source url="<%= Discourse.base_url %><%= @topic_view.relative_url %>.rss"><%= @topic_view.title %></source>
+      </item>
+    <% end %>
+  </channel>
+</rss>
diff --git a/config/routes.rb b/config/routes.rb
index 8229cd2e4..b153d30c2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -176,6 +176,7 @@ Discourse::Application.routes.draw do
   put 't/:topic_id/unmute' => 'topics#unmute', :constraints => {:topic_id => /\d+/}
 
   get 't/:topic_id/:post_number' => 'topics#show', :constraints => {:topic_id => /\d+/, :post_number => /\d+/}
+  get 't/:slug/:topic_id.rss' => 'topics#feed', :format => :rss, :constraints => {:topic_id => /\d+/}
   get 't/:slug/:topic_id' => 'topics#show', :constraints => {:topic_id => /\d+/}
   get 't/:slug/:topic_id/:post_number' => 'topics#show', :constraints => {:topic_id => /\d+/, :post_number => /\d+/}
   post 't/:topic_id/timings' => 'topics#timings', :constraints => {:topic_id => /\d+/}
diff --git a/lib/topic_view.rb b/lib/topic_view.rb
index 5504aef58..4fec47d64 100644
--- a/lib/topic_view.rb
+++ b/lib/topic_view.rb
@@ -234,6 +234,10 @@ class TopicView
     @highest_post_number ||= @all_posts.maximum(:post_number)
   end
 
+  def recent_posts
+    @all_posts.by_newest.with_user.first(25)
+  end
+
   protected
 
   def read_posts_set
diff --git a/spec/components/topic_view_spec.rb b/spec/components/topic_view_spec.rb
index d9d495721..090241f4e 100644
--- a/spec/components/topic_view_spec.rb
+++ b/spec/components/topic_view_spec.rb
@@ -283,5 +283,24 @@ describe TopicView do
 
   end
 
+  context '#recent_posts' do
+    before do
+      24.times do # our let()s have already created 3
+        Fabricate(:post, topic: topic, user: first_poster)
+      end
+    end
+    it 'returns at most 25 recent posts ordered newest first' do
+      recent_posts = topic_view.recent_posts
+
+      # count
+      recent_posts.count.should == 25
+
+      # ordering
+      recent_posts.include?(p1).should be_false
+      recent_posts.include?(p3).should be_true
+      recent_posts.first.created_at.should > recent_posts.last.created_at
+    end
+  end
+
 end
 
diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb
index fccd19ae2..f73527e02 100644
--- a/spec/controllers/topics_controller_spec.rb
+++ b/spec/controllers/topics_controller_spec.rb
@@ -299,6 +299,16 @@ describe TopicsController do
 
   end
 
+  describe '#feed' do
+    let(:topic) { Fabricate(:post).topic }
+
+    it 'renders rss of the topic' do
+      get :feed, topic_id: topic.id, slug: 'foo', format: :rss
+      response.should be_success
+      response.content_type.should == 'application/rss+xml'
+    end
+  end
+
   describe 'update' do
     it "won't allow us to update a topic when we're not logged in" do
       lambda { xhr :put, :update, topic_id: 1, slug: 'xyz' }.should raise_error(Discourse::NotLoggedIn)
diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb
index daaa57ecc..375eec091 100644
--- a/spec/models/post_spec.rb
+++ b/spec/models/post_spec.rb
@@ -23,6 +23,24 @@ describe Post do
 
   it_behaves_like "a versioned model"
 
+  describe 'scopes' do
+
+    describe '#by_newest' do
+      it 'returns posts ordered by created_at desc' do
+        2.times { Fabricate(:post) }
+        Post.by_newest.first.created_at.should > Post.by_newest.last.created_at
+      end
+    end
+
+    describe '#with_user' do
+      it 'gives you a user' do
+        Fabricate(:post, user: Fabricate(:user))
+        Post.with_user.first.user.should be_a User
+      end
+    end
+
+  end
+
   describe 'post uniqueness' do
 
     context "disabled" do
@@ -760,5 +778,11 @@ describe Post do
 
   end
 
+  describe '#readable_author' do
+    it 'delegates to the associated user' do
+      User.any_instance.expects(:readable_name)
+      Fabricate(:post).author_readable
+    end
+  end
 
 end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index ef56c1071..6ca68b59c 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -716,4 +716,22 @@ describe User do
 
   end
 
+  describe '#readable_name' do
+    context 'when name is missing' do
+      it 'returns just the username' do
+        Fabricate(:user, username: 'foo', name: nil).readable_name.should == 'foo'
+      end
+    end
+    context 'when name and username are identical' do
+      it 'returns just the username' do
+        Fabricate(:user, username: 'foo', name: 'foo').readable_name.should == 'foo'
+      end
+    end
+    context 'when name and username are not identical' do
+      it 'returns the name and username' do
+        Fabricate(:user, username: 'foo', name: 'Bar Baz').readable_name.should == 'Bar Baz (foo)'
+      end
+    end
+  end
+
 end