From c16b8364ab0edd83f19a9930e1fe9612a9f49875 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Wed, 17 Sep 2014 11:18:41 -0400
Subject: [PATCH] FIX: Support ember app routing to topics with only slugs

---
 .../javascripts/discourse/models/topic.js     |  7 ++++---
 .../discourse/routes/application_routes.js    |  1 +
 .../discourse/routes/topic-by-slug.js.es6     |  9 +++++++++
 app/controllers/topics_controller.rb          |  7 +++++++
 config/routes.rb                              |  1 +
 spec/controllers/topics_controller_spec.rb    | 20 +++++++++++++++++++
 .../helpers/create-pretender.js.es6           |  4 ++++
 .../javascripts/integration/topic-test.js.es6 | 11 +++++++---
 8 files changed, 54 insertions(+), 6 deletions(-)
 create mode 100644 app/assets/javascripts/discourse/routes/topic-by-slug.js.es6

diff --git a/app/assets/javascripts/discourse/models/topic.js b/app/assets/javascripts/discourse/models/topic.js
index 5a0d37223..9eab5f1a9 100644
--- a/app/assets/javascripts/discourse/models/topic.js
+++ b/app/assets/javascripts/discourse/models/topic.js
@@ -458,9 +458,10 @@ Discourse.Topic.reopenClass({
 
   resetNew: function() {
     return Discourse.ajax("/topics/reset-new", {type: 'PUT'});
+  },
+
+  idForSlug: function(slug) {
+    return Discourse.ajax("/t/id_for/" + slug);
   }
 
-
 });
-
-
diff --git a/app/assets/javascripts/discourse/routes/application_routes.js b/app/assets/javascripts/discourse/routes/application_routes.js
index 2a0cd141c..d235940dd 100644
--- a/app/assets/javascripts/discourse/routes/application_routes.js
+++ b/app/assets/javascripts/discourse/routes/application_routes.js
@@ -17,6 +17,7 @@ Discourse.Route.buildRoutes(function() {
     this.route('fromParams', { path: '/' });
     this.route('fromParamsNear', { path: '/:nearPost' });
   });
+  this.resource('topicBySlug', { path: '/t/:slug' });
 
   this.resource('discovery', { path: '/' }, function() {
     router = this;
diff --git a/app/assets/javascripts/discourse/routes/topic-by-slug.js.es6 b/app/assets/javascripts/discourse/routes/topic-by-slug.js.es6
new file mode 100644
index 000000000..04d6cd1d6
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/topic-by-slug.js.es6
@@ -0,0 +1,9 @@
+export default Ember.Route.extend({
+  model: function(params) {
+    return Discourse.Topic.idForSlug(params.slug);
+  },
+
+  afterModel: function(result) {
+    Discourse.URL.routeTo(result.url);
+  }
+});
diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb
index 557a9c6f4..24402ca66 100644
--- a/app/controllers/topics_controller.rb
+++ b/app/controllers/topics_controller.rb
@@ -30,6 +30,13 @@ class TopicsController < ApplicationController
 
   skip_before_filter :check_xhr, only: [:show, :feed]
 
+  def id_for_slug
+    topic = Topic.find_by(slug: params[:slug].downcase)
+    guardian.ensure_can_see!(topic)
+    raise Discourse::NotFound unless topic
+    render json: {slug: topic.slug, topic_id: topic.id, url: topic.url}
+  end
+
   def show
     flash["referer"] ||= request.referer
 
diff --git a/config/routes.rb b/config/routes.rb
index 13222d2c8..aaf51b3f8 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -356,6 +356,7 @@ Discourse::Application.routes.draw do
   get 'embed/count' => 'embed#count'
 
   # Topic routes
+  get "t/id_for/:slug" => "topics#id_for_slug"
   get "t/:slug/:topic_id/wordpress" => "topics#wordpress", constraints: {topic_id: /\d+/}
   get "t/:slug/:topic_id/moderator-liked" => "topics#moderator_liked", constraints: {topic_id: /\d+/}
   get "t/:topic_id/wordpress" => "topics#wordpress", constraints: {topic_id: /\d+/}
diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb
index 0d0649586..b67b458b0 100644
--- a/spec/controllers/topics_controller_spec.rb
+++ b/spec/controllers/topics_controller_spec.rb
@@ -542,6 +542,26 @@ describe TopicsController do
     end
   end
 
+  describe 'id_for_slug' do
+    let(:topic) { Fabricate(:post).topic }
+
+    it "returns JSON for the slug" do
+      xhr :get, :id_for_slug, slug: topic.slug
+      response.should be_success
+      json = ::JSON.parse(response.body)
+      json.should be_present
+      json['topic_id'].should == topic.id
+      json['url'].should == topic.url
+      json['slug'].should == topic.slug
+    end
+
+    it "returns invalid access if the user can't see the topic" do
+      Guardian.any_instance.expects(:can_see?).with(topic).returns(false)
+      xhr :get, :id_for_slug, slug: topic.slug
+      response.should_not be_success
+    end
+  end
+
   describe 'show' do
 
     let(:topic) { Fabricate(:post).topic }
diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6
index 3a0ca4ef8..9f3181333 100644
--- a/test/javascripts/helpers/create-pretender.js.es6
+++ b/test/javascripts/helpers/create-pretender.js.es6
@@ -34,6 +34,10 @@ export default function() {
       }
     });
 
+    this.get("/t/id_for/:slug", function() {
+      return response({id: 280, slug: "internationalization-localization", url: "/t/internationalization-localization/280"});
+    });
+
     this.get("/404-body", function() {
       return [200, {"Content-Type": "text/html"}, "<div class='page-not-found'>not found</div>"];
     });
diff --git a/test/javascripts/integration/topic-test.js.es6 b/test/javascripts/integration/topic-test.js.es6
index c57021bf9..503d3f912 100644
--- a/test/javascripts/integration/topic-test.js.es6
+++ b/test/javascripts/integration/topic-test.js.es6
@@ -1,11 +1,16 @@
 integration("View Topic");
 
 test("Enter a Topic", function() {
-  expect(2);
-
   visit("/t/internationalization-localization/280");
   andThen(function() {
-    ok(exists("#topic"), "The was rendered");
+    ok(exists("#topic"), "The topic was rendered");
     ok(exists("#topic .post-cloak"), "The topic has cloaked posts");
   });
 });
+
+test("Enter without an id", function() {
+  visit("/t/internationalization-localization");
+  andThen(function() {
+    ok(exists("#topic"), "The topic was rendered");
+  });
+});