Use the same component for similar topics as search results.

This commit is contained in:
Robin Ward 2015-06-24 15:08:22 -04:00
parent b4960d48b4
commit 6422d5efbd
14 changed files with 162 additions and 103 deletions

View file

@ -332,7 +332,7 @@ export default Ember.ObjectController.extend(Presence, {
this.set('similarTopicsMessage', message); this.set('similarTopicsMessage', message);
} }
this.store.find('topic', {similar: {title, raw: body}}).then(function(newTopics) { this.store.find('similar-topic', {title, raw: body}).then(function(newTopics) {
similarTopics.clear(); similarTopics.clear();
similarTopics.pushObjects(newTopics.get('content')); similarTopics.pushObjects(newTopics.get('content'));

View file

@ -109,7 +109,6 @@ Discourse.URL = Ember.Object.createWithMixins({
@param {String} path The path we are routing to. @param {String} path The path we are routing to.
**/ **/
routeTo: function(path) { routeTo: function(path) {
if (Em.isEmpty(path)) { return; } if (Em.isEmpty(path)) { return; }
if (Discourse.get('requiresRefresh')) { if (Discourse.get('requiresRefresh')) {

View file

@ -143,10 +143,13 @@ export default Ember.Object.extend({
return this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest'); return this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
}, },
_lookupSubType(subType, id, root) { _lookupSubType(subType, type, id, root) {
// cheat: we know we already have categories in memory // cheat: we know we already have categories in memory
if (subType === 'category') { // TODO: topics do their own resolving of `category_id`
// to category. That should either respect this or be
// removed.
if (subType === 'category' && type !== 'topic') {
return Discourse.Category.findById(id); return Discourse.Category.findById(id);
} }
@ -172,13 +175,13 @@ export default Ember.Object.extend({
} }
}, },
_hydrateEmbedded(obj, root) { _hydrateEmbedded(type, obj, root) {
const self = this; const self = this;
Object.keys(obj).forEach(function(k) { Object.keys(obj).forEach(function(k) {
const m = /(.+)\_id$/.exec(k); const m = /(.+)\_id$/.exec(k);
if (m) { if (m) {
const subType = m[1]; const subType = m[1];
const hydrated = self._lookupSubType(subType, obj[k], root); const hydrated = self._lookupSubType(subType, type, obj[k], root);
if (hydrated) { if (hydrated) {
obj[subType] = hydrated; obj[subType] = hydrated;
delete obj[k]; delete obj[k];
@ -196,7 +199,7 @@ export default Ember.Object.extend({
// Experimental: If serialized with a certain option we'll wire up embedded objects // Experimental: If serialized with a certain option we'll wire up embedded objects
// automatically. // automatically.
if (root.__rest_serializer === "1") { if (root.__rest_serializer === "1") {
this._hydrateEmbedded(obj, root); this._hydrateEmbedded(type, obj, root);
} }
const existing = fromMap(type, obj.id); const existing = fromMap(type, obj.id);

View file

@ -16,6 +16,7 @@ function findTopicList(store, filter, filterParams, extras) {
extras = extras || {}; extras = extras || {};
return new Ember.RSVP.Promise(function(resolve) { return new Ember.RSVP.Promise(function(resolve) {
const session = Discourse.Session.current(); const session = Discourse.Session.current();
if (extras.cached) { if (extras.cached) {
@ -80,7 +81,6 @@ export default function(filter, extras) {
}, },
model(data, transition) { model(data, transition) {
// attempt to stop early cause we need this to be called before .sync // attempt to stop early cause we need this to be called before .sync
Discourse.ScreenTrack.current().stop(); Discourse.ScreenTrack.current().stop();

View file

@ -2,11 +2,5 @@
<h3>{{i18n 'composer.similar_topics'}}</h3> <h3>{{i18n 'composer.similar_topics'}}</h3>
<ul class='topics'> <ul class='topics'>
{{#each similarTopics as |t|}} {{search-result-topic results=similarTopics}}
<li>
{{topic-status topic=t}}
{{topic-link t}}
{{category-link t.category}}
</li>
{{/each}}
</ul> </ul>

View file

@ -74,6 +74,17 @@
.posts-count { .posts-count {
background-color: scale-color($tertiary, $lightness: -40%); background-color: scale-color($tertiary, $lightness: -40%);
} }
ul {
list-style: none;
margin: 0;
padding: 0;
}
.search-link {
.fa, .blurb {
color: scale-color($tertiary, $lightness: -40%);
}
}
} }
.composer-popup:nth-of-type(2) { .composer-popup:nth-of-type(2) {

View file

@ -0,0 +1,39 @@
require_dependency 'similar_topic_serializer'
require_dependency 'search/grouped_search_results'
class SimilarTopicsController < ApplicationController
class SimilarTopic
def initialize(topic)
@topic = topic
end
attr_reader :topic
def blurb
Search::GroupedSearchResults.blurb_for(@topic.try(:blurb))
end
end
def index
params.require(:title)
params.require(:raw)
title, raw = params[:title], params[:raw]
[:title, :raw].each { |key| check_length_of(key, params[key]) }
# Only suggest similar topics if the site has a minimum amount of topics present.
return render json: [] unless Topic.count_exceeds_minimum?
topics = Topic.similar_to(title, raw, current_user).to_a
topics.map! {|t| SimilarTopic.new(t) }
render_serialized(topics, SimilarTopicSerializer, root: :similar_topics, rest_serializer: true)
end
protected
def check_length_of(key, attr)
str = (key == :raw) ? "body" : key.to_s
raise Discourse::InvalidParameters.new(key) if attr.length < SiteSetting.send("min_#{str}_similar_length")
end
end

View file

@ -149,19 +149,6 @@ class TopicsController < ApplicationController
success ? render_serialized(topic, BasicTopicSerializer) : render_json_error(topic) success ? render_serialized(topic, BasicTopicSerializer) : render_json_error(topic)
end end
def similar_to
params.require(:title)
params.require(:raw)
title, raw = params[:title], params[:raw]
[:title, :raw].each { |key| check_length_of(key, params[key]) }
# Only suggest similar topics if the site has a minimum amount of topics present.
return render json: [] unless Topic.count_exceeds_minimum?
topics = Topic.similar_to(title, raw, current_user).to_a
render_serialized(topics, TopicListItemSerializer, root: :topics)
end
def feature_stats def feature_stats
params.require(:category_id) params.require(:category_id)
category_id = params[:category_id].to_i category_id = params[:category_id].to_i
@ -510,11 +497,6 @@ class TopicsController < ApplicationController
topic.move_posts(current_user, post_ids_including_replies, args) topic.move_posts(current_user, post_ids_including_replies, args)
end end
def check_length_of(key, attr)
str = (key == :raw) ? "body" : key.to_s
invalid_param(key) if attr.length < SiteSetting.send("min_#{str}_similar_length")
end
def check_for_status_presence(key, attr) def check_for_status_presence(key, attr)
invalid_param(key) unless %w(pinned pinned_globally visible closed archived).include?(attr) invalid_param(key) unless %w(pinned pinned_globally visible closed archived).include?(attr)
end end

View file

@ -397,7 +397,7 @@ class Topic < ActiveRecord::Base
return [] unless candidate_ids.present? return [] unless candidate_ids.present?
similar = Topic.select(sanitize_sql_array(["topics.*, similarity(topics.title, :title) + similarity(topics.title, :raw) AS similarity", title: title, raw: raw])) similar = Topic.select(sanitize_sql_array(["topics.*, similarity(topics.title, :title) + similarity(topics.title, :raw) AS similarity, p.cooked as blurb", title: title, raw: raw]))
.joins("JOIN posts AS p ON p.topic_id = topics.id AND p.post_number = 1") .joins("JOIN posts AS p ON p.topic_id = topics.id AND p.post_number = 1")
.limit(SiteSetting.max_similar_results) .limit(SiteSetting.max_similar_results)
.where("topics.id IN (?)", candidate_ids) .where("topics.id IN (?)", candidate_ids)

View file

@ -0,0 +1,17 @@
class SimilarTopicSerializer < ApplicationSerializer
has_one :topic, serializer: TopicListItemSerializer, embed: :ids
attributes :id, :blurb, :created_at
def id
object.topic.id
end
def blurb
object.blurb
end
def created_at
object.topic.created_at
end
end

View file

@ -410,7 +410,10 @@ Discourse::Application.routes.draw do
put "topics/bulk" put "topics/bulk"
put "topics/reset-new" => 'topics#reset_new' put "topics/reset-new" => 'topics#reset_new'
post "topics/timings" post "topics/timings"
get "topics/similar_to"
get 'topics/similar_to' => 'similar_topics#index'
resources :similar_topics
get "topics/feature_stats" get "topics/feature_stats"
get "topics/created-by/:username" => "list#topics_by", as: "topics_by", constraints: {username: USERNAME_ROUTE_FORMAT} get "topics/created-by/:username" => "list#topics_by", as: "topics_by", constraints: {username: USERNAME_ROUTE_FORMAT}
get "topics/private-messages/:username" => "list#private_messages", as: "topics_private_messages", constraints: {username: USERNAME_ROUTE_FORMAT} get "topics/private-messages/:username" => "list#private_messages", as: "topics_private_messages", constraints: {username: USERNAME_ROUTE_FORMAT}

View file

@ -3,7 +3,6 @@ require 'sanitize'
class Search class Search
class GroupedSearchResults class GroupedSearchResults
include ActiveModel::Serialization include ActiveModel::Serialization
class TextHelper class TextHelper
@ -26,11 +25,7 @@ class Search
end end
def blurb(post) def blurb(post)
cooked = SearchObserver::HtmlScrubber.scrub(post.cooked).squish GroupedSearchResults.blurb_for(post.cooked, @term)
terms = @term.split(/\s+/)
blurb = TextHelper.excerpt(cooked, terms.first, radius: 100)
blurb = TextHelper.truncate(cooked, length: 200) if blurb.blank?
Sanitize.clean(blurb)
end end
def add(object) def add(object)
@ -43,6 +38,18 @@ class Search
end end
end end
def self.blurb_for(cooked, term=nil)
cooked = SearchObserver::HtmlScrubber.scrub(cooked).squish
blurb = nil
if term
terms = term.split(/\s+/)
blurb = TextHelper.excerpt(cooked, terms.first, radius: 100)
end
blurb = TextHelper.truncate(cooked, length: 200) if blurb.blank?
Sanitize.clean(blurb)
end
end end
end end

View file

@ -0,0 +1,66 @@
require 'spec_helper'
describe SimilarTopicsController do
context 'similar_to' do
let(:title) { 'this title is long enough to search for' }
let(:raw) { 'this body is long enough to search for' }
it "requires a title" do
expect { xhr :get, :index, raw: raw }.to raise_error(ActionController::ParameterMissing)
end
it "requires a raw body" do
expect { xhr :get, :index, title: title }.to raise_error(ActionController::ParameterMissing)
end
it "raises an error if the title length is below the minimum" do
SiteSetting.stubs(:min_title_similar_length).returns(100)
expect { xhr :get, :index, title: title, raw: raw }.to raise_error(Discourse::InvalidParameters)
end
it "raises an error if the body length is below the minimum" do
SiteSetting.stubs(:min_body_similar_length).returns(100)
expect { xhr :get, :index, title: title, raw: raw }.to raise_error(Discourse::InvalidParameters)
end
describe "minimum_topics_similar" do
before do
SiteSetting.stubs(:minimum_topics_similar).returns(30)
end
after do
xhr :get, :index, title: title, raw: raw
end
describe "With enough topics" do
before do
Topic.stubs(:count).returns(50)
end
it "deletes to Topic.similar_to if there are more topics than `minimum_topics_similar`" do
Topic.expects(:similar_to).with(title, raw, nil).returns([Fabricate(:topic)])
end
describe "with a logged in user" do
let(:user) { log_in }
it "passes a user through if logged in" do
Topic.expects(:similar_to).with(title, raw, user).returns([Fabricate(:topic)])
end
end
end
it "does not call Topic.similar_to if there are fewer topics than `minimum_topics_similar`" do
Topic.stubs(:count).returns(10)
Topic.expects(:similar_to).never
end
end
end
end

View file

@ -245,68 +245,6 @@ describe TopicsController do
end end
end end
context 'similar_to' do
let(:title) { 'this title is long enough to search for' }
let(:raw) { 'this body is long enough to search for' }
it "requires a title" do
expect { xhr :get, :similar_to, raw: raw }.to raise_error(ActionController::ParameterMissing)
end
it "requires a raw body" do
expect { xhr :get, :similar_to, title: title }.to raise_error(ActionController::ParameterMissing)
end
it "raises an error if the title length is below the minimum" do
SiteSetting.stubs(:min_title_similar_length).returns(100)
expect { xhr :get, :similar_to, title: title, raw: raw }.to raise_error(Discourse::InvalidParameters)
end
it "raises an error if the body length is below the minimum" do
SiteSetting.stubs(:min_body_similar_length).returns(100)
expect { xhr :get, :similar_to, title: title, raw: raw }.to raise_error(Discourse::InvalidParameters)
end
describe "minimum_topics_similar" do
before do
SiteSetting.stubs(:minimum_topics_similar).returns(30)
end
after do
xhr :get, :similar_to, title: title, raw: raw
end
describe "With enough topics" do
before do
Topic.stubs(:count).returns(50)
end
it "deletes to Topic.similar_to if there are more topics than `minimum_topics_similar`" do
Topic.expects(:similar_to).with(title, raw, nil).returns([Fabricate(:topic)])
end
describe "with a logged in user" do
let(:user) { log_in }
it "passes a user through if logged in" do
Topic.expects(:similar_to).with(title, raw, user).returns([Fabricate(:topic)])
end
end
end
it "does not call Topic.similar_to if there are fewer topics than `minimum_topics_similar`" do
Topic.stubs(:count).returns(10)
Topic.expects(:similar_to).never
end
end
end
context 'clear_pin' do context 'clear_pin' do
it 'needs you to be logged in' do it 'needs you to be logged in' do
expect { xhr :put, :clear_pin, topic_id: 1 }.to raise_error(Discourse::NotLoggedIn) expect { xhr :put, :clear_pin, topic_id: 1 }.to raise_error(Discourse::NotLoggedIn)