Debugging Tool for Hot Topics

This commit is contained in:
Robin Ward 2013-04-02 16:52:51 -04:00
parent 161fdcb364
commit ee76f1926d
16 changed files with 244 additions and 42 deletions

View File

@ -7,7 +7,8 @@
@module Discourse @module Discourse
**/ **/
Discourse.ListTopicsController = Discourse.ObjectController.extend({ Discourse.ListTopicsController = Discourse.ObjectController.extend({
needs: ['list', 'composer'], needs: ['list', 'composer', 'modal'],
// If we're changing our channel // If we're changing our channel
previousChannel: null, previousChannel: null,
@ -50,6 +51,14 @@ Discourse.ListTopicsController = Discourse.ObjectController.extend({
topic.toggleStar(); topic.toggleStar();
}, },
// Show rank details
showRankDetails: function(topic) {
var modalController = this.get('controllers.modal');
if (modalController) {
modalController.show(Discourse.TopicRankDetailsView.create({ topic: topic }));
}
},
createTopic: function() { createTopic: function() {
this.get('controllers.list').createTopic(); this.get('controllers.list').createTopic();
}, },

View File

@ -180,6 +180,30 @@ Handlebars.registerHelper('editDate', function(property, options) {
} }
}); });
/**
Displays a percentile based on a `percent_rank` field
@method percentile
@for Ember.Handlebars
**/
Ember.Handlebars.registerHelper('percentile', function(property, options) {
var percentile = Ember.Handlebars.get(this, property, options);
return Math.round((1.0 - percentile) * 100)
});
/**
Displays a float nicely
@method float
@for Ember.Handlebars
**/
Ember.Handlebars.registerHelper('float', function(property, options) {
var x = Ember.Handlebars.get(this, property, options);
if (!x) return "0";
if (Math.round(x) === x) return x;
return x.toFixed(3)
});
/** /**
Display logic for numbers. Display logic for numbers.

View File

@ -17,7 +17,12 @@
{{#if unseen}} {{#if unseen}}
<a href="{{lastReadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new}}'><i class='icon icon-asterisk'></i></a> <a href="{{lastReadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new}}'><i class='icon icon-asterisk'></i></a>
{{/if}} {{/if}}
{{#if rank_details}}
<i class='icon icon-beaker score' {{action showRankDetails this}} title='{{i18n rank_details.show}}'></i>
{{/if}}
</td> </td>
<td class='category'> <td class='category'>
{{categoryLink category}} {{categoryLink category}}
</td> </td>

View File

@ -0,0 +1,46 @@
{{#with view.topic.rank_details}}
<div class="modal-body">
<!-- Note this isn't translated because it's a debugging tool for a feature
that is not complete yet. We will probably rip this out altogether -->
<table class='table'>
<tr>
<td>hot topic type</td>
<td>
{{hot_topic_type}}
</td>
</tr>
<tr>
<td>random bias</td>
<td>{{float random_bias}}</td>
</tr>
<tr>
<td>random multiplier</td>
<td>{{float random_multiplier}}</td>
</tr>
<tr>
<td>days ago bias</td>
<td>{{float days_ago_bias}}</td>
</tr>
<tr>
<td>days ago multiplier</td>
<td>{{float days_ago_multiplier}}</td>
</tr>
<tr>
<td>ranking formula</td>
<td>
<p>= (random_bias * random_multiplier) +<br/>
(days_ago_bias * days_ago_multiplier)</p>
<p>= ({{float random_bias}} * {{float random_multiplier}}) + ({{float days_ago_bias}} * {{float days_ago_multiplier}})</p>
</td>
</tr>
<tr>
<td>ranking score</td>
<td><b>{{float ranking_score}}</b></td>
</tr>
</table>
</div>
{{/with}}

View File

@ -0,0 +1,13 @@
/**
A modal view for displaying the ranking details of a topic
@class TopicRankDetailsView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.TopicRankDetailsView = Discourse.ModalBodyView.extend({
templateName: 'modal/topic_rank_details',
title: Em.String.i18n('rank_details.title')
});

View File

@ -151,6 +151,8 @@
.archetype-option { .archetype-option {
margin-bottom: 20px; margin-bottom: 20px;
} }
} }
.password-confirmation { .password-confirmation {
display: none; display: none;

View File

@ -97,7 +97,20 @@
.main-link { .main-link {
width: 515px; width: 515px;
font-size: 16px; font-size: 16px;
&:hover i.score {
display: inline-block;
}
i.score {
color: green;
cursor: pointer;
display: none;
}
} }
@include medium-width { @include medium-width {
.main-link { .main-link {
width: 400px; width: 400px;

View File

@ -28,7 +28,6 @@ class ListController < ApplicationController
end end
def category def category
query = TopicQuery.new(current_user, page: params[:page]) query = TopicQuery.new(current_user, page: params[:page])
list = nil list = nil

View File

@ -18,9 +18,23 @@ class HotTopic < ActiveRecord::Base
no_old_in_first_x_rows = 8 # don't show old results in the first x rows no_old_in_first_x_rows = 8 # don't show old results in the first x rows
# Include all sticky uncategorized on Hot # Include all sticky uncategorized on Hot
exec_sql("INSERT INTO hot_topics (topic_id, score) exec_sql("INSERT INTO hot_topics (topic_id,
SELECT t.id, RANDOM() random_bias,
random_multiplier,
days_ago_bias,
days_ago_multiplier,
score,
hot_topic_type)
SELECT t.id,
calc.random_bias,
1.0,
0,
1.0,
calc.random_bias,
1
FROM topics AS t FROM topics AS t
INNER JOIN (SELECT id, RANDOM() as random_bias
FROM topics) AS calc ON calc.id = t.id
WHERE t.deleted_at IS NULL WHERE t.deleted_at IS NULL
AND t.visible AND t.visible
AND (NOT t.archived) AND (NOT t.archived)
@ -28,12 +42,27 @@ class HotTopic < ActiveRecord::Base
AND t.category_id IS NULL") AND t.category_id IS NULL")
# Include high percentile recent topics # Include high percentile recent topics
inserted_count = exec_sql("INSERT INTO hot_topics (topic_id, category_id, score) inserted_count = exec_sql("INSERT INTO hot_topics (topic_id,
category_id,
random_bias,
random_multiplier,
days_ago_bias,
days_ago_multiplier,
score,
hot_topic_type)
SELECT t.id, SELECT t.id,
t.category_id, t.category_id,
((1.0 - (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP-t.created_at)/86400) / :days_ago) * 0.95) + calc.random_bias,
(RANDOM() * 0.05) 0.05,
calc.days_ago_bias,
0.95,
(calc.random_bias * 0.05) + (days_ago_bias * 0.95),
2
FROM topics AS t FROM topics AS t
INNER JOIN (SELECT id,
RANDOM() as random_bias,
((1.0 - (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP-created_at)/86400) / :days_ago) * 0.95) AS days_ago_bias
FROM topics) AS calc ON calc.id = t.id
WHERE t.deleted_at IS NULL WHERE t.deleted_at IS NULL
AND t.visible AND t.visible
AND (NOT t.closed) AND (NOT t.closed)
@ -56,16 +85,26 @@ class HotTopic < ActiveRecord::Base
max_old_score = HotTopic.order('score desc').limit(no_old_in_first_x_rows).last.score max_old_score = HotTopic.order('score desc').limit(no_old_in_first_x_rows).last.score
end end
# Add a sprinkling of random older topics # Add a sprinkling of random older topics
exec_sql("INSERT INTO hot_topics (topic_id, category_id, score) exec_sql("INSERT INTO hot_topics (topic_id,
category_id,
random_bias,
random_multiplier,
days_ago_bias,
days_ago_multiplier,
score,
hot_topic_type)
SELECT t.id, SELECT t.id,
t.category_id, t.category_id,
RANDOM() * :max_old_score calc.random_bias,
:max_old_score,
0,
1.0,
calc.random_bias * :max_old_score,
3
FROM topics AS t FROM topics AS t
INNER JOIN (SELECT id, RANDOM() as random_bias
FROM topics) AS calc ON calc.id = t.id
WHERE t.deleted_at IS NULL WHERE t.deleted_at IS NULL
AND t.visible AND t.visible
AND (NOT t.closed) AND (NOT t.closed)

View File

@ -55,6 +55,7 @@ class Topic < ActiveRecord::Base
# When we want to temporarily attach some data to a forum topic (usually before serialization) # When we want to temporarily attach some data to a forum topic (usually before serialization)
attr_accessor :user_data attr_accessor :user_data
attr_accessor :posters # TODO: can replace with posters_summary once we remove old list code attr_accessor :posters # TODO: can replace with posters_summary once we remove old list code
attr_accessor :topic_list
# The regular order # The regular order

View File

@ -3,9 +3,14 @@ require_dependency 'avatar_lookup'
class TopicList class TopicList
include ActiveModel::Serialization include ActiveModel::Serialization
attr_accessor :more_topics_url, :draft, :draft_key, :draft_sequence attr_accessor :more_topics_url,
:draft,
:draft_key,
:draft_sequence,
:filter
def initialize(current_user, topics) def initialize(filter, current_user, topics)
@filter = filter
@current_user = current_user @current_user = current_user
@topics_input = topics @topics_input = topics
end end
@ -30,6 +35,7 @@ class TopicList
@topics.each do |ft| @topics.each do |ft|
ft.user_data = @topic_lookup[ft.id] if @topic_lookup.present? ft.user_data = @topic_lookup[ft.id] if @topic_lookup.present?
ft.posters = ft.posters_summary(ft.user_data, @current_user, avatar_lookup: avatar_lookup) ft.posters = ft.posters_summary(ft.user_data, @current_user, avatar_lookup: avatar_lookup)
ft.topic_list = self
end end
return @topics return @topics

View File

@ -10,7 +10,8 @@ class TopicListItemSerializer < ListableTopicSerializer
:archived, :archived,
:starred, :starred,
:has_best_of, :has_best_of,
:archetype :archetype,
:rank_details
has_one :category has_one :category
has_many :posters, serializer: TopicPosterSerializer, embed: :objects has_many :posters, serializer: TopicPosterSerializer, embed: :objects
@ -20,6 +21,35 @@ class TopicListItemSerializer < ListableTopicSerializer
end end
alias :include_starred? :seen alias :include_starred? :seen
# This is for debugging / tweaking the hot topic rankings.
# We will likely remove it after we are happier with things.
def rank_details
hot_topic_type = case object.hot_topic.hot_topic_type
when 1 then 'sticky'
when 2 then 'recent high scoring'
when 3 then 'old high scoring'
end
{topic_score: object.score,
percent_rank: object.percent_rank,
random_bias: object.hot_topic.random_bias,
random_multiplier: object.hot_topic.random_multiplier,
days_ago_bias: object.hot_topic.days_ago_bias,
days_ago_multiplier: object.hot_topic.days_ago_multiplier,
ranking_score: object.hot_topic.score,
hot_topic_type: hot_topic_type}
end
def include_rank_details?
return false unless object.topic_list.present?
return false unless scope.user.present?
return false unless scope.user.admin?
object.topic_list.filter == :hot
end
def posters def posters
object.posters || [] object.posters || []
end end
@ -28,4 +58,5 @@ class TopicListItemSerializer < ListableTopicSerializer
PinnedCheck.new(object, object.user_data).pinned? PinnedCheck.new(object, object.user_data).pinned?
end end
end end

View File

@ -1,6 +1,11 @@
class TopicListSerializer < ApplicationSerializer class TopicListSerializer < ApplicationSerializer
attributes :can_create_topic, :more_topics_url, :filter_summary, :draft, :draft_key, :draft_sequence attributes :can_create_topic,
:more_topics_url,
:filter_summary,
:draft,
:draft_key,
:draft_sequence
has_many :topics, serializer: TopicListItemSerializer, embed: :objects has_many :topics, serializer: TopicListItemSerializer, embed: :objects

View File

@ -386,6 +386,10 @@ en:
favorited: "There are no more favorited topics to read." favorited: "There are no more favorited topics to read."
category: "There are no more {{category}} topics." category: "There are no more {{category}} topics."
rank_details:
show: show topic rank details
title: Topic Rank Details
topic: topic:
create_in: 'Create {{categoryName}} Topic' create_in: 'Create {{categoryName}} Topic'
create: 'Create Topic' create: 'Create Topic'
@ -407,6 +411,7 @@ en:
description: "Sorry, we couldn't find that topic. Perhaps it was removed by a moderator?" description: "Sorry, we couldn't find that topic. Perhaps it was removed by a moderator?"
unread_posts: "you have {{unread}} unread old posts in this topic" unread_posts: "you have {{unread}} unread old posts in this topic"
new_posts: "there are {{new_posts}} new posts in this topic since you last read it" new_posts: "there are {{new_posts}} new posts in this topic since you last read it"
likes: likes:
one: "there is 1 like in this topic" one: "there is 1 like in this topic"
other: "there are {{count}} likes in this topic" other: "there are {{count}} likes in this topic"

View File

@ -0,0 +1,9 @@
class AddValuesToHotTopics < ActiveRecord::Migration
def change
add_column :hot_topics, :random_bias, :float
add_column :hot_topics, :random_multiplier, :float
add_column :hot_topics, :days_ago_bias, :float
add_column :hot_topics, :days_ago_multiplier, :float
add_column :hot_topics, :hot_topic_type, :integer
end
end

View File

@ -81,7 +81,7 @@ class TopicQuery
# If not logged in, return some random results, preferably in this category # If not logged in, return some random results, preferably in this category
if @user.blank? if @user.blank?
return TopicList.new(@user, random_suggested_results_for(topic, SiteSetting.suggested_topics, exclude_topic_ids)) return TopicList.new(:suggested, @user, random_suggested_results_for(topic, SiteSetting.suggested_topics, exclude_topic_ids))
end end
results = unread_results(per_page: SiteSetting.suggested_topics) results = unread_results(per_page: SiteSetting.suggested_topics)
@ -118,49 +118,45 @@ class TopicQuery
end end
end end
TopicList.new(@user, results) TopicList.new(:suggested, @user, results)
end end
# The latest view of topics # The latest view of topics
def list_latest def list_latest
TopicList.new(@user, default_list) create_list(:latest)
end end
# The favorited topics # The favorited topics
def list_favorited def list_favorited
return_list do |list| create_list(:favorited) {|topics| topics.where('tu.starred') }
list.where('tu.starred')
end
end end
def list_read def list_read
return_list(unordered: true) do |list| create_list(:read, unordered: true) do |topics|
list.order('COALESCE(tu.last_visited_at, topics.bumped_at) DESC') topics.order('COALESCE(tu.last_visited_at, topics.bumped_at) DESC')
end end
end end
def list_hot def list_hot
return_list(unordered: true) do |list| create_list(:hot, unordered: true) do |topics|
# Find hot topics topics.joins(:hot_topic).order(TopicQuery.order_hotness)
list = list.joins(:hot_topic)
.order(TopicQuery.order_hotness)
end end
end end
def list_new def list_new
TopicList.new(@user, new_results) TopicList.new(:new, @user, new_results)
end end
def list_unread def list_unread
TopicList.new(@user, unread_results) TopicList.new(:unread, @user, unread_results)
end end
def list_posted def list_posted
return_list {|l| l.where('tu.user_id IS NOT NULL') } create_list(:posted) {|l| l.where('tu.user_id IS NOT NULL') }
end end
def list_uncategorized def list_uncategorized
return_list(unordered: true) do |list| create_list(:uncategorized, unordered: true) do |list|
list = list.where(category_id: nil) list = list.where(category_id: nil)
if @user_id.present? if @user_id.present?
@ -172,7 +168,7 @@ class TopicQuery
end end
def list_category(category) def list_category(category)
return_list(unordered: true) do |list| create_list(:category, unordered: true) do |list|
list = list.where(category_id: category.id) list = list.where(category_id: category.id)
if @user_id.present? if @user_id.present?
list.order(TopicQuery.order_with_pinned_sql) list.order(TopicQuery.order_with_pinned_sql)
@ -191,13 +187,15 @@ class TopicQuery
end end
def list_new_in_category(category) def list_new_in_category(category)
return_list {|l| l.where(category_id: category.id).by_newest.first(25)} create_list(:new_in_category) {|l| l.where(category_id: category.id).by_newest.first(25)}
end end
protected protected
def return_list(list_opts={}) def create_list(filter, list_opts={})
TopicList.new(@user, yield(default_list(list_opts))) topics = default_list(list_opts)
topics = yield(topics) if block_given?
TopicList.new(filter, @user, topics)
end end
# Create a list based on a bunch of detault options # Create a list based on a bunch of detault options
@ -233,7 +231,6 @@ class TopicQuery
end end
def new_results(list_opts={}) def new_results(list_opts={})
default_list(list_opts) default_list(list_opts)
.where("topics.created_at >= :created_at", created_at: @user.treat_as_new_topic_start_date) .where("topics.created_at >= :created_at", created_at: @user.treat_as_new_topic_start_date)
.where("tu.last_read_post_number IS NULL") .where("tu.last_read_post_number IS NULL")
@ -252,12 +249,10 @@ class TopicQuery
.where(closed: false, archived: false, visible: true) .where(closed: false, archived: false, visible: true)
if topic.category_id.present? if topic.category_id.present?
results = results.order("CASE WHEN topics.category_id = #{topic.category_id.to_i} THEN 0 ELSE 1 END, RANDOM()") return results.order("CASE WHEN topics.category_id = #{topic.category_id.to_i} THEN 0 ELSE 1 END, RANDOM()")
else
results = results.order("RANDOM()")
end end
results results.order("RANDOM()")
end end
end end