Track clicks on topics in search results

This commit is contained in:
Robin Ward 2017-07-17 15:42:32 -04:00
parent 21e02d6969
commit cdb3706025
8 changed files with 187 additions and 26 deletions

View File

@ -4,6 +4,7 @@ import { avatarImg } from 'discourse/widgets/post';
import DiscourseURL from 'discourse/lib/url';
import { wantsNewWindow } from 'discourse/lib/intercept-click';
import { applySearchAutocomplete } from "discourse/lib/search";
import { ajax } from 'discourse/lib/ajax';
import { h } from 'virtual-dom';
@ -249,7 +250,29 @@ export default createWidget('header', {
},
linkClickedEvent(attrs) {
if (!(attrs && attrs.searchContextEnabled)) this.closeAll();
let searchContextEnabled = false;
if (attrs) {
searchContextEnabled = attrs.searchContextEnabled;
const { searchLogId, searchResultId, searchResultType } = attrs;
if (searchLogId && searchResultId && searchResultType) {
ajax('/search/click', {
type: 'POST',
data: {
search_log_id: searchLogId,
search_result_id: searchResultId,
search_result_type: searchResultType
}
});
}
}
if (!searchContextEnabled) {
this.closeAll();
}
this.updateHighlight();
},

View File

@ -20,18 +20,26 @@ class Highlighted extends RawHtml {
}
}
function createSearchResult(type, linkField, fn) {
function createSearchResult({ type, linkField, builder }) {
return createWidget(`search-result-${type}`, {
html(attrs) {
return attrs.results.map(r => {
let searchResultId;
if (type === "topic") {
searchResultId = r.get('topic_id');
}
return h('li', this.attach('link', {
href: r.get(linkField),
contents: () => fn.call(this, r, attrs.term),
contents: () => builder.call(this, r, attrs.term),
className: 'search-link',
searchContextEnabled: this.attrs.searchContextEnabled
searchResultId,
searchResultType: type,
searchContextEnabled: attrs.searchContextEnabled,
searchLogId: attrs.searchLogId
}));
});
}
},
});
}
@ -47,27 +55,43 @@ function postResult(result, link, term) {
return html;
}
createSearchResult('user', 'path', function(u) {
return [ avatarImg('small', { template: u.avatar_template, username: u.username }), ' ', h('span.user-results', h('b', u.username)), ' ', h('span.user-results', u.name ? u.name : '') ];
createSearchResult({
type: 'user',
linkField: 'path',
builder(u) {
return [ avatarImg('small', { template: u.avatar_template, username: u.username }), ' ', h('span.user-results', h('b', u.username)), ' ', h('span.user-results', u.name ? u.name : '') ];
}
});
createSearchResult('topic', 'url', function(result, term) {
const topic = result.topic;
const link = h('span.topic', [
this.attach('topic-status', { topic, disableActions: true }),
h('span.topic-title', new Highlighted(topic.get('fancyTitle'), term)),
this.attach('category-link', { category: topic.get('category'), link: false })
]);
createSearchResult({
type: 'topic',
linkField: 'url',
builder(result, term) {
const topic = result.topic;
const link = h('span.topic', [
this.attach('topic-status', { topic, disableActions: true }),
h('span.topic-title', new Highlighted(topic.get('fancyTitle'), term)),
this.attach('category-link', { category: topic.get('category'), link: false })
]);
return postResult.call(this, result, link, term);
return postResult.call(this, result, link, term);
}
});
createSearchResult('post', 'url', function(result, term) {
return postResult.call(this, result, I18n.t('search.post_format', result), term);
createSearchResult({
type: 'post',
linkField: 'url',
builder(result, term) {
return postResult.call(this, result, I18n.t('search.post_format', result), term);
}
});
createSearchResult('category', 'url', function (c) {
return this.attach('category-link', { category: c, link: false });
createSearchResult({
type: 'category',
linkField: 'url',
builder(c) {
return this.attach('category-link', { category: c, link: false });
}
});
createWidget('search-menu-results', {
@ -102,7 +126,8 @@ createWidget('search-menu-results', {
return [
h('ul', this.attach(rt.componentName, {
searchContextEnabled: this.attrs.searchContextEnabled,
searchContextEnabled: attrs.searchContextEnabled,
searchLogId: attrs.results.grouped_search_result.search_log_id,
results: rt.results,
term: attrs.term
})),

View File

@ -127,11 +127,13 @@ export default createWidget('search-menu', {
if (searchData.loading) {
results.push(h('div.searching', h('div.spinner')));
} else {
results.push(this.attach('search-menu-results', { term: searchData.term,
noResults: searchData.noResults,
results: searchData.results,
invalidTerm: searchData.invalidTerm,
searchContextEnabled: searchData.contextEnabled }));
results.push(this.attach('search-menu-results', {
term: searchData.term,
noResults: searchData.noResults,
results: searchData.results,
invalidTerm: searchData.invalidTerm,
searchContextEnabled: searchData.contextEnabled
}));
}
}

View File

@ -67,6 +67,27 @@ class SearchController < ApplicationController
render_serialized(result, GroupedSearchResultSerializer, result: result)
end
def click
params.require(:search_log_id)
params.require(:search_result_type)
params.require(:search_result_id)
if params[:search_result_type] == 'topic'
where = { id: params[:search_log_id] }
if current_user.present?
where[:user_id] = current_user.id
else
where[:ip_address] = request.remote_ip
end
SearchLog.where(where).update_all(
clicked_topic_id: params[:search_result_id]
)
end
render json: success_json
end
protected
def lookup_search_context

View File

@ -8,4 +8,8 @@ class GroupedSearchResultSerializer < ApplicationSerializer
object.search_log_id
end
def include_search_log_id?
search_log_id.present?
end
end

View File

@ -550,6 +550,7 @@ Discourse::Application.routes.draw do
get "top" => "list#top"
get "search/query" => "search#query"
get "search" => "search#show"
post "search/click" => "search#click"
# Topics resource
get "t/:id" => "topics#show"

View File

@ -123,4 +123,89 @@ describe SearchController do
end
context "#click" do
it "doesn't work wthout the necessary parameters" do
expect(-> {
xhr :post, :click
}).to raise_error(ActionController::ParameterMissing)
end
it "doesn't record the click for a different user" do
log_in(:user)
_, search_log_id = SearchLog.log(
term: 'kitty',
search_type: :header,
user_id: -10,
ip_address: '127.0.0.1'
)
xhr :post, :click, {
search_log_id: search_log_id,
search_result_id: 12345,
search_result_type: 'topic'
}
expect(response).to be_success
expect(SearchLog.find(search_log_id).clicked_topic_id).to be_blank
end
it "records the click for a logged in user" do
user = log_in(:user)
_, search_log_id = SearchLog.log(
term: 'kitty',
search_type: :header,
user_id: user.id,
ip_address: '127.0.0.1'
)
xhr :post, :click, {
search_log_id: search_log_id,
search_result_id: 12345,
search_result_type: 'topic'
}
expect(response).to be_success
expect(SearchLog.find(search_log_id).clicked_topic_id).to eq(12345)
end
it "records the click for an anonymous user" do
request.stubs(:remote_ip).returns('192.168.0.1')
_, search_log_id = SearchLog.log(
term: 'kitty',
search_type: :header,
ip_address: '192.168.0.1'
)
xhr :post, :click, {
search_log_id: search_log_id,
search_result_id: 22222,
search_result_type: 'topic'
}
expect(response).to be_success
expect(SearchLog.find(search_log_id).clicked_topic_id).to eq(22222)
end
it "doesn't record the click for a different IP" do
request.stubs(:remote_ip).returns('192.168.0.2')
_, search_log_id = SearchLog.log(
term: 'kitty',
search_type: :header,
ip_address: '192.168.0.1'
)
xhr :post, :click, {
search_log_id: search_log_id,
search_result_id: 22222,
search_result_type: 'topic'
}
expect(response).to be_success
expect(SearchLog.find(search_log_id).clicked_topic_id).to be_blank
end
end
end

View File

@ -72,4 +72,4 @@ QUnit.test("Search with context", assert => {
andThen(() => {
assert.ok(!$('.search-context input[type=checkbox]').is(":checked"));
});
});
});