diff --git a/app/assets/javascripts/discourse/controllers/list_topics_controller.js b/app/assets/javascripts/discourse/controllers/list_topics_controller.js index 8d666f67f04..f7ad3045918 100644 --- a/app/assets/javascripts/discourse/controllers/list_topics_controller.js +++ b/app/assets/javascripts/discourse/controllers/list_topics_controller.js @@ -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(); }, diff --git a/app/assets/javascripts/discourse/helpers/application_helpers.js b/app/assets/javascripts/discourse/helpers/application_helpers.js index fcc9857d4e3..866cdf5a17d 100644 --- a/app/assets/javascripts/discourse/helpers/application_helpers.js +++ b/app/assets/javascripts/discourse/helpers/application_helpers.js @@ -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. diff --git a/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars b/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars index b63c19e70d1..93bd48db894 100644 --- a/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars +++ b/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars @@ -17,7 +17,12 @@ {{#if unseen}} {{/if}} + + {{#if rank_details}} + + {{/if}} + {{categoryLink category}} diff --git a/app/assets/javascripts/discourse/templates/modal/topic_rank_details.js.handlebars b/app/assets/javascripts/discourse/templates/modal/topic_rank_details.js.handlebars new file mode 100644 index 00000000000..411d3fb62be --- /dev/null +++ b/app/assets/javascripts/discourse/templates/modal/topic_rank_details.js.handlebars @@ -0,0 +1,46 @@ +{{#with view.topic.rank_details}} + +{{/with}} \ No newline at end of file diff --git a/app/assets/javascripts/discourse/views/modal/topic_rank_details_view.js b/app/assets/javascripts/discourse/views/modal/topic_rank_details_view.js new file mode 100644 index 00000000000..39095330989 --- /dev/null +++ b/app/assets/javascripts/discourse/views/modal/topic_rank_details_view.js @@ -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') + +}); diff --git a/app/assets/stylesheets/application/modal.css.scss b/app/assets/stylesheets/application/modal.css.scss index 0ec27a48205..3ce7872b037 100644 --- a/app/assets/stylesheets/application/modal.css.scss +++ b/app/assets/stylesheets/application/modal.css.scss @@ -151,6 +151,8 @@ .archetype-option { margin-bottom: 20px; } + + } .password-confirmation { display: none; diff --git a/app/assets/stylesheets/application/topic-list.css.scss b/app/assets/stylesheets/application/topic-list.css.scss index 7c99dfc2a31..2bbb6fa998a 100755 --- a/app/assets/stylesheets/application/topic-list.css.scss +++ b/app/assets/stylesheets/application/topic-list.css.scss @@ -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; diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 773e05c89cc..951c4e871ce 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -28,7 +28,6 @@ class ListController < ApplicationController end def category - query = TopicQuery.new(current_user, page: params[:page]) list = nil diff --git a/app/models/hot_topic.rb b/app/models/hot_topic.rb index 8a231ea62ce..c4ef81774a6 100644 --- a/app/models/hot_topic.rb +++ b/app/models/hot_topic.rb @@ -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) diff --git a/app/models/topic.rb b/app/models/topic.rb index adbc2c84cbb..af082c77126 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -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 diff --git a/app/models/topic_list.rb b/app/models/topic_list.rb index c5f9d448ccb..4fc096d0ff1 100644 --- a/app/models/topic_list.rb +++ b/app/models/topic_list.rb @@ -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 diff --git a/app/serializers/topic_list_item_serializer.rb b/app/serializers/topic_list_item_serializer.rb index eeed912515f..e30f8e4e9d2 100644 --- a/app/serializers/topic_list_item_serializer.rb +++ b/app/serializers/topic_list_item_serializer.rb @@ -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 diff --git a/app/serializers/topic_list_serializer.rb b/app/serializers/topic_list_serializer.rb index a4c5cb39ea5..f08b4d373b4 100644 --- a/app/serializers/topic_list_serializer.rb +++ b/app/serializers/topic_list_serializer.rb @@ -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 diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index bde01d8efa4..4213783efc3 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -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" diff --git a/db/migrate/20130402210723_add_values_to_hot_topics.rb b/db/migrate/20130402210723_add_values_to_hot_topics.rb new file mode 100644 index 00000000000..dab4598a6c6 --- /dev/null +++ b/db/migrate/20130402210723_add_values_to_hot_topics.rb @@ -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 diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 9f69ff1126b..e091b99dbb3 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -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