mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-23 23:58:31 -05:00
Use the same component for similar topics as search results.
This commit is contained in:
parent
b4960d48b4
commit
6422d5efbd
14 changed files with 162 additions and 103 deletions
|
@ -332,7 +332,7 @@ export default Ember.ObjectController.extend(Presence, {
|
|||
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.pushObjects(newTopics.get('content'));
|
||||
|
||||
|
|
|
@ -109,7 +109,6 @@ Discourse.URL = Ember.Object.createWithMixins({
|
|||
@param {String} path The path we are routing to.
|
||||
**/
|
||||
routeTo: function(path) {
|
||||
|
||||
if (Em.isEmpty(path)) { return; }
|
||||
|
||||
if (Discourse.get('requiresRefresh')) {
|
||||
|
|
|
@ -143,10 +143,13 @@ export default Ember.Object.extend({
|
|||
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
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -172,13 +175,13 @@ export default Ember.Object.extend({
|
|||
}
|
||||
},
|
||||
|
||||
_hydrateEmbedded(obj, root) {
|
||||
_hydrateEmbedded(type, obj, root) {
|
||||
const self = this;
|
||||
Object.keys(obj).forEach(function(k) {
|
||||
const m = /(.+)\_id$/.exec(k);
|
||||
if (m) {
|
||||
const subType = m[1];
|
||||
const hydrated = self._lookupSubType(subType, obj[k], root);
|
||||
const hydrated = self._lookupSubType(subType, type, obj[k], root);
|
||||
if (hydrated) {
|
||||
obj[subType] = hydrated;
|
||||
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
|
||||
// automatically.
|
||||
if (root.__rest_serializer === "1") {
|
||||
this._hydrateEmbedded(obj, root);
|
||||
this._hydrateEmbedded(type, obj, root);
|
||||
}
|
||||
|
||||
const existing = fromMap(type, obj.id);
|
||||
|
|
|
@ -16,6 +16,7 @@ function findTopicList(store, filter, filterParams, extras) {
|
|||
|
||||
extras = extras || {};
|
||||
return new Ember.RSVP.Promise(function(resolve) {
|
||||
|
||||
const session = Discourse.Session.current();
|
||||
|
||||
if (extras.cached) {
|
||||
|
@ -80,7 +81,6 @@ export default function(filter, extras) {
|
|||
},
|
||||
|
||||
model(data, transition) {
|
||||
|
||||
// attempt to stop early cause we need this to be called before .sync
|
||||
Discourse.ScreenTrack.current().stop();
|
||||
|
||||
|
|
|
@ -2,11 +2,5 @@
|
|||
<h3>{{i18n 'composer.similar_topics'}}</h3>
|
||||
|
||||
<ul class='topics'>
|
||||
{{#each similarTopics as |t|}}
|
||||
<li>
|
||||
{{topic-status topic=t}}
|
||||
{{topic-link t}}
|
||||
{{category-link t.category}}
|
||||
</li>
|
||||
{{/each}}
|
||||
{{search-result-topic results=similarTopics}}
|
||||
</ul>
|
||||
|
|
|
@ -74,6 +74,17 @@
|
|||
.posts-count {
|
||||
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) {
|
||||
|
|
39
app/controllers/similar_topics_controller.rb
Normal file
39
app/controllers/similar_topics_controller.rb
Normal 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
|
|
@ -149,19 +149,6 @@ class TopicsController < ApplicationController
|
|||
success ? render_serialized(topic, BasicTopicSerializer) : render_json_error(topic)
|
||||
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
|
||||
params.require(:category_id)
|
||||
category_id = params[:category_id].to_i
|
||||
|
@ -510,11 +497,6 @@ class TopicsController < ApplicationController
|
|||
topic.move_posts(current_user, post_ids_including_replies, args)
|
||||
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)
|
||||
invalid_param(key) unless %w(pinned pinned_globally visible closed archived).include?(attr)
|
||||
end
|
||||
|
|
|
@ -397,7 +397,7 @@ class Topic < ActiveRecord::Base
|
|||
|
||||
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")
|
||||
.limit(SiteSetting.max_similar_results)
|
||||
.where("topics.id IN (?)", candidate_ids)
|
||||
|
|
17
app/serializers/similar_topic_serializer.rb
Normal file
17
app/serializers/similar_topic_serializer.rb
Normal 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
|
|
@ -410,7 +410,10 @@ Discourse::Application.routes.draw do
|
|||
put "topics/bulk"
|
||||
put "topics/reset-new" => 'topics#reset_new'
|
||||
post "topics/timings"
|
||||
get "topics/similar_to"
|
||||
|
||||
get 'topics/similar_to' => 'similar_topics#index'
|
||||
resources :similar_topics
|
||||
|
||||
get "topics/feature_stats"
|
||||
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}
|
||||
|
|
|
@ -3,7 +3,6 @@ require 'sanitize'
|
|||
class Search
|
||||
|
||||
class GroupedSearchResults
|
||||
|
||||
include ActiveModel::Serialization
|
||||
|
||||
class TextHelper
|
||||
|
@ -26,11 +25,7 @@ class Search
|
|||
end
|
||||
|
||||
def blurb(post)
|
||||
cooked = SearchObserver::HtmlScrubber.scrub(post.cooked).squish
|
||||
terms = @term.split(/\s+/)
|
||||
blurb = TextHelper.excerpt(cooked, terms.first, radius: 100)
|
||||
blurb = TextHelper.truncate(cooked, length: 200) if blurb.blank?
|
||||
Sanitize.clean(blurb)
|
||||
GroupedSearchResults.blurb_for(post.cooked, @term)
|
||||
end
|
||||
|
||||
def add(object)
|
||||
|
@ -43,6 +38,18 @@ class Search
|
|||
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
|
||||
|
|
66
spec/controllers/similar_topics_controller_spec.rb
Normal file
66
spec/controllers/similar_topics_controller_spec.rb
Normal 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
|
||||
|
|
@ -245,68 +245,6 @@ describe TopicsController do
|
|||
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
|
||||
it 'needs you to be logged in' do
|
||||
expect { xhr :put, :clear_pin, topic_id: 1 }.to raise_error(Discourse::NotLoggedIn)
|
||||
|
|
Loading…
Reference in a new issue