Debugging Tool for Hot Topics
This commit is contained in:
parent
161fdcb364
commit
ee76f1926d
|
@ -7,7 +7,8 @@
|
|||
@module Discourse
|
||||
**/
|
||||
Discourse.ListTopicsController = Discourse.ObjectController.extend({
|
||||
needs: ['list', 'composer'],
|
||||
needs: ['list', 'composer', 'modal'],
|
||||
|
||||
// If we're changing our channel
|
||||
previousChannel: null,
|
||||
|
||||
|
@ -50,6 +51,14 @@ Discourse.ListTopicsController = Discourse.ObjectController.extend({
|
|||
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() {
|
||||
this.get('controllers.list').createTopic();
|
||||
},
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -17,7 +17,12 @@
|
|||
{{#if unseen}}
|
||||
<a href="{{lastReadUrl}}" class='badge new-posts badge-notification' title='{{i18n topic.new}}'><i class='icon icon-asterisk'></i></a>
|
||||
{{/if}}
|
||||
|
||||
{{#if rank_details}}
|
||||
<i class='icon icon-beaker score' {{action showRankDetails this}} title='{{i18n rank_details.show}}'></i>
|
||||
{{/if}}
|
||||
</td>
|
||||
|
||||
<td class='category'>
|
||||
{{categoryLink category}}
|
||||
</td>
|
||||
|
|
|
@ -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}}
|
|
@ -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')
|
||||
|
||||
});
|
|
@ -151,6 +151,8 @@
|
|||
.archetype-option {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.password-confirmation {
|
||||
display: none;
|
||||
|
|
|
@ -97,7 +97,20 @@
|
|||
.main-link {
|
||||
width: 515px;
|
||||
font-size: 16px;
|
||||
|
||||
&:hover i.score {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
i.score {
|
||||
color: green;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@include medium-width {
|
||||
.main-link {
|
||||
width: 400px;
|
||||
|
|
|
@ -28,7 +28,6 @@ class ListController < ApplicationController
|
|||
end
|
||||
|
||||
def category
|
||||
|
||||
query = TopicQuery.new(current_user, page: params[:page])
|
||||
list = nil
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
# Include all sticky uncategorized on Hot
|
||||
exec_sql("INSERT INTO hot_topics (topic_id, score)
|
||||
SELECT t.id, RANDOM()
|
||||
exec_sql("INSERT INTO hot_topics (topic_id,
|
||||
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
|
||||
INNER JOIN (SELECT id, RANDOM() as random_bias
|
||||
FROM topics) AS calc ON calc.id = t.id
|
||||
WHERE t.deleted_at IS NULL
|
||||
AND t.visible
|
||||
AND (NOT t.archived)
|
||||
|
@ -28,12 +42,27 @@ class HotTopic < ActiveRecord::Base
|
|||
AND t.category_id IS NULL")
|
||||
|
||||
# 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,
|
||||
t.category_id,
|
||||
((1.0 - (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP-t.created_at)/86400) / :days_ago) * 0.95) +
|
||||
(RANDOM() * 0.05)
|
||||
calc.random_bias,
|
||||
0.05,
|
||||
calc.days_ago_bias,
|
||||
0.95,
|
||||
(calc.random_bias * 0.05) + (days_ago_bias * 0.95),
|
||||
2
|
||||
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
|
||||
AND t.visible
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 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,
|
||||
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
|
||||
INNER JOIN (SELECT id, RANDOM() as random_bias
|
||||
FROM topics) AS calc ON calc.id = t.id
|
||||
WHERE t.deleted_at IS NULL
|
||||
AND t.visible
|
||||
AND (NOT t.closed)
|
||||
|
|
|
@ -55,6 +55,7 @@ class Topic < ActiveRecord::Base
|
|||
# When we want to temporarily attach some data to a forum topic (usually before serialization)
|
||||
attr_accessor :user_data
|
||||
attr_accessor :posters # TODO: can replace with posters_summary once we remove old list code
|
||||
attr_accessor :topic_list
|
||||
|
||||
|
||||
# The regular order
|
||||
|
|
|
@ -3,9 +3,14 @@ require_dependency 'avatar_lookup'
|
|||
class TopicList
|
||||
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
|
||||
@topics_input = topics
|
||||
end
|
||||
|
@ -30,6 +35,7 @@ class TopicList
|
|||
@topics.each do |ft|
|
||||
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.topic_list = self
|
||||
end
|
||||
|
||||
return @topics
|
||||
|
|
|
@ -10,7 +10,8 @@ class TopicListItemSerializer < ListableTopicSerializer
|
|||
:archived,
|
||||
:starred,
|
||||
:has_best_of,
|
||||
:archetype
|
||||
:archetype,
|
||||
:rank_details
|
||||
|
||||
has_one :category
|
||||
has_many :posters, serializer: TopicPosterSerializer, embed: :objects
|
||||
|
@ -20,6 +21,35 @@ class TopicListItemSerializer < ListableTopicSerializer
|
|||
end
|
||||
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
|
||||
object.posters || []
|
||||
end
|
||||
|
@ -28,4 +58,5 @@ class TopicListItemSerializer < ListableTopicSerializer
|
|||
PinnedCheck.new(object, object.user_data).pinned?
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
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
|
||||
|
||||
|
|
|
@ -386,6 +386,10 @@ en:
|
|||
favorited: "There are no more favorited topics to read."
|
||||
category: "There are no more {{category}} topics."
|
||||
|
||||
rank_details:
|
||||
show: show topic rank details
|
||||
title: Topic Rank Details
|
||||
|
||||
topic:
|
||||
create_in: 'Create {{categoryName}} Topic'
|
||||
create: 'Create Topic'
|
||||
|
@ -407,6 +411,7 @@ en:
|
|||
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"
|
||||
new_posts: "there are {{new_posts}} new posts in this topic since you last read it"
|
||||
|
||||
likes:
|
||||
one: "there is 1 like in this topic"
|
||||
other: "there are {{count}} likes in this topic"
|
||||
|
|
|
@ -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
|
|
@ -81,7 +81,7 @@ class TopicQuery
|
|||
|
||||
# If not logged in, return some random results, preferably in this category
|
||||
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
|
||||
|
||||
results = unread_results(per_page: SiteSetting.suggested_topics)
|
||||
|
@ -118,49 +118,45 @@ class TopicQuery
|
|||
end
|
||||
end
|
||||
|
||||
TopicList.new(@user, results)
|
||||
TopicList.new(:suggested, @user, results)
|
||||
end
|
||||
|
||||
# The latest view of topics
|
||||
def list_latest
|
||||
TopicList.new(@user, default_list)
|
||||
create_list(:latest)
|
||||
end
|
||||
|
||||
# The favorited topics
|
||||
def list_favorited
|
||||
return_list do |list|
|
||||
list.where('tu.starred')
|
||||
end
|
||||
create_list(:favorited) {|topics| topics.where('tu.starred') }
|
||||
end
|
||||
|
||||
def list_read
|
||||
return_list(unordered: true) do |list|
|
||||
list.order('COALESCE(tu.last_visited_at, topics.bumped_at) DESC')
|
||||
create_list(:read, unordered: true) do |topics|
|
||||
topics.order('COALESCE(tu.last_visited_at, topics.bumped_at) DESC')
|
||||
end
|
||||
end
|
||||
|
||||
def list_hot
|
||||
return_list(unordered: true) do |list|
|
||||
# Find hot topics
|
||||
list = list.joins(:hot_topic)
|
||||
.order(TopicQuery.order_hotness)
|
||||
create_list(:hot, unordered: true) do |topics|
|
||||
topics.joins(:hot_topic).order(TopicQuery.order_hotness)
|
||||
end
|
||||
end
|
||||
|
||||
def list_new
|
||||
TopicList.new(@user, new_results)
|
||||
TopicList.new(:new, @user, new_results)
|
||||
end
|
||||
|
||||
def list_unread
|
||||
TopicList.new(@user, unread_results)
|
||||
TopicList.new(:unread, @user, unread_results)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def list_uncategorized
|
||||
return_list(unordered: true) do |list|
|
||||
create_list(:uncategorized, unordered: true) do |list|
|
||||
list = list.where(category_id: nil)
|
||||
|
||||
if @user_id.present?
|
||||
|
@ -172,7 +168,7 @@ class TopicQuery
|
|||
end
|
||||
|
||||
def list_category(category)
|
||||
return_list(unordered: true) do |list|
|
||||
create_list(:category, unordered: true) do |list|
|
||||
list = list.where(category_id: category.id)
|
||||
if @user_id.present?
|
||||
list.order(TopicQuery.order_with_pinned_sql)
|
||||
|
@ -191,13 +187,15 @@ class TopicQuery
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
protected
|
||||
|
||||
def return_list(list_opts={})
|
||||
TopicList.new(@user, yield(default_list(list_opts)))
|
||||
def create_list(filter, list_opts={})
|
||||
topics = default_list(list_opts)
|
||||
topics = yield(topics) if block_given?
|
||||
TopicList.new(filter, @user, topics)
|
||||
end
|
||||
|
||||
# Create a list based on a bunch of detault options
|
||||
|
@ -233,7 +231,6 @@ class TopicQuery
|
|||
end
|
||||
|
||||
def new_results(list_opts={})
|
||||
|
||||
default_list(list_opts)
|
||||
.where("topics.created_at >= :created_at", created_at: @user.treat_as_new_topic_start_date)
|
||||
.where("tu.last_read_post_number IS NULL")
|
||||
|
@ -252,12 +249,10 @@ class TopicQuery
|
|||
.where(closed: false, archived: false, visible: true)
|
||||
|
||||
if topic.category_id.present?
|
||||
results = results.order("CASE WHEN topics.category_id = #{topic.category_id.to_i} THEN 0 ELSE 1 END, RANDOM()")
|
||||
else
|
||||
results = results.order("RANDOM()")
|
||||
return results.order("CASE WHEN topics.category_id = #{topic.category_id.to_i} THEN 0 ELSE 1 END, RANDOM()")
|
||||
end
|
||||
|
||||
results
|
||||
results.order("RANDOM()")
|
||||
end
|
||||
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue