work in progress wide category list

This commit is contained in:
Sam
2013-10-17 17:44:56 +11:00
parent 7bf96ee690
commit 1ee49798b2
17 changed files with 200 additions and 25 deletions

View File

@@ -34,6 +34,7 @@ Discourse.Category = Discourse.Model.extend({
return Discourse.getURL("/category/") + (this.get('slug')); return Discourse.getURL("/category/") + (this.get('slug'));
}.property('name'), }.property('name'),
style: function() { style: function() {
return "background-color: #" + (this.get('category.color')) + "; color: #" + (this.get('category.text_color')) + ";"; return "background-color: #" + (this.get('category.color')) + "; color: #" + (this.get('category.text_color')) + ";";
}.property('color', 'text_color'), }.property('color', 'text_color'),
@@ -101,7 +102,15 @@ Discourse.Category = Discourse.Model.extend({
latestTopic: function(){ latestTopic: function(){
return this.get("topics")[0]; return this.get("topics")[0];
}.property("topics") }.property("topics"),
unreadTopics: function(){
return Discourse.TopicTrackingState.current().countUnread(this.get('name'));
}.property('Discourse.TopicTrackingState.current.messageCount'),
newTopics: function(){
return Discourse.TopicTrackingState.current().countNew(this.get('name'));
}.property('Discourse.TopicTrackingState.current.messageCount')
}); });

View File

@@ -87,6 +87,10 @@ Discourse.Topic = Discourse.Model.extend({
return this.urlForPostNumber(this.get('highest_post_number')); return this.urlForPostNumber(this.get('highest_post_number'));
}.property('url', 'highest_post_number'), }.property('url', 'highest_post_number'),
lastPosterUrl: function() {
return Discourse.getURL("/users/") + this.get("last_poster.username");
}.property('last_poster'),
// The amount of new posts to display. It might be different than what the server // The amount of new posts to display. It might be different than what the server
// tells us if we are still asynchronously flushing our "recently read" data. // tells us if we are still asynchronously flushing our "recently read" data.
// So take what the browser has seen into consideration. // So take what the browser has seen into consideration.

View File

@@ -129,20 +129,23 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
this.set("messageCount", this.get("messageCount") + 1); this.set("messageCount", this.get("messageCount") + 1);
}, },
countNew: function(){ countNew: function(category_name){
return _.chain(this.states) return _.chain(this.states)
.where({last_read_post_number: null}) .where({last_read_post_number: null})
.where(function(topic){ return topic.category_name === category_name || !category_name;})
.value() .value()
.length; .length;
}, },
countUnread: function(){ countUnread: function(category_name){
var count = 0; return _.chain(this.states)
_.each(this.states, function(topic){ .where(function(topic){
count += (topic.last_read_post_number !== null && return topic.last_read_post_number !== null &&
topic.last_read_post_number < topic.highest_post_number) ? 1 : 0; topic.last_read_post_number < topic.highest_post_number;
}); })
return count; .where(function(topic){ return topic.category_name === category_name || !category_name;})
.value()
.length;
}, },
countCategory: function(category) { countCategory: function(category) {

View File

@@ -46,7 +46,7 @@
<a href="{{unbound url}}" {{{bindAttr class=":age ageCold"}}} title='{{i18n first_post}}: {{{unboundDate created_at}}}' >{{unboundAge created_at}}</a> <a href="{{unbound url}}" {{{bindAttr class=":age ageCold"}}} title='{{i18n first_post}}: {{{unboundDate created_at}}}' >{{unboundAge created_at}}</a>
</td> </td>
<td class='num activity last'> <td class='num activity last'>
<a href="{{unbound lastPostUrl}}" class='age' title='{{i18n last_post}}: {{{unboundDate bumped_at}}}'>{{unboundAge bumped_at}}</a> <a href="{{unbound lastPosterUrl}}" class='age' title='{{i18n last_post}}: {{{unboundDate bumped_at}}}'>{{unboundAge bumped_at}}</a>
</td> </td>
{{else}} {{else}}
<td class='num activity'> <td class='num activity'>
@@ -62,4 +62,4 @@
<div class='alert alert-info'> <div class='alert alert-info'>
{{i18n choose_topic.none_found}} {{i18n choose_topic.none_found}}
</div> </div>
{{/if}} {{/if}}

View File

@@ -14,6 +14,12 @@
{{#each model.categories}} {{#each model.categories}}
<tr> <tr>
<td class='category'>{{categoryLink this}} <td class='category'>{{categoryLink this}}
{{#if unreadTopics}}
<a href={{unbound url}} class='badge new-posts badge-notification' title='{{i18n topic.unread_topics count="unreadTopics"}}'>{{unbound unreadTopics}}</a>
{{/if}}
{{#if newTopics}}
<a href={{unbound url}} class='badge new-posts badge-notification' title='{{i18n topic.new_topics count="newTopics"}}'>{{unbound newTopics}} <i class='icon icon-asterisk'></i></a>
{{/if}}
{{#if description_excerpt}} {{#if description_excerpt}}
<div class="category-description"> <div class="category-description">
{{{description_excerpt}}} {{{description_excerpt}}}
@@ -25,12 +31,15 @@
{{/each}} {{/each}}
</td> </td>
<td>{{number topic_count}}</td> <td>{{number topic_count}}</td>
<td>{{number posts_total}}</td> <td>{{number post_count}}</td>
{{#with latestTopic}} {{#with latestTopic}}
<td {{bindAttr class="archived"}}> <td {{bindAttr class="archived"}}>
{{topicStatus topic=this}} {{topicStatus topic=this}}
{{{topicLink this}}} {{{topicLink this}}}
<div class='lastUserInfo'>
{{i18n categories.by}} <a href="{{{unbound lastPosterUrl}}}">{{unbound last_poster.username}}</a>
{{unboundAge last_posted_at}}
</div>
</td> </td>
{{/with}} {{/with}}
</tr> </tr>

View File

@@ -9,7 +9,14 @@ class CategoriesController < ApplicationController
def index def index
@description = SiteSetting.site_description @description = SiteSetting.site_description
@list = CategoryList.new(guardian) wide_mode = SiteSetting.enable_wide_category_list
options = {}
options[:latest_post_only] = params[:latest_post_only] || wide_mode
@list = CategoryList.new(guardian,options)
@list.draft_key = Draft::NEW_TOPIC @list.draft_key = Draft::NEW_TOPIC
@list.draft_sequence = DraftSequence.current(current_user, Draft::NEW_TOPIC) @list.draft_sequence = DraftSequence.current(current_user, Draft::NEW_TOPIC)

View File

@@ -13,6 +13,7 @@ class Category < ActiveRecord::Base
end end
belongs_to :user belongs_to :user
belongs_to :latest_post, class_name: "Post"
has_many :topics has_many :topics
has_many :category_featured_topics has_many :category_featured_topics
@@ -227,6 +228,26 @@ SQL
end end
end end
def update_latest
latest_post_id = Post
.order("posts.created_at desc")
.where("NOT hidden")
.joins("join topics on topics.id = topic_id")
.where("topics.category_id = :id", id: self.id)
.limit(1)
.pluck("posts.id")
.first
latest_topic_id = Topic
.order("topics.created_at desc")
.where("visible")
.where("topics.category_id = :id", id: self.id)
.limit(1)
.pluck("topics.id")
.first
self.update_attributes(latest_topic_id: latest_topic_id, latest_post_id: latest_post_id)
end
def self.resolve_permissions(permissions) def self.resolve_permissions(permissions)
read_restricted = true read_restricted = true

View File

@@ -8,10 +8,11 @@ class CategoryList
:draft_key, :draft_key,
:draft_sequence :draft_sequence
def initialize(guardian=nil) def initialize(guardian=nil, options = {})
@guardian = guardian || Guardian.new @guardian = guardian || Guardian.new
@options = options
find_relevant_topics find_relevant_topics unless latest_post_only?
find_categories find_categories
prune_empty prune_empty
@@ -21,6 +22,10 @@ class CategoryList
private private
def latest_post_only?
@options[:latest_post_only]
end
# Retrieve a list of all the topics we'll need # Retrieve a list of all the topics we'll need
def find_relevant_topics def find_relevant_topics
@topics_by_category_id = {} @topics_by_category_id = {}
@@ -47,16 +52,35 @@ class CategoryList
.order('COALESCE(categories.topics_month, 0) DESC') .order('COALESCE(categories.topics_month, 0) DESC')
.order('COALESCE(categories.topics_year, 0) DESC') .order('COALESCE(categories.topics_year, 0) DESC')
if latest_post_only?
@categories = @categories.includes(:latest_post => :topic )
end
@categories = @categories.to_a @categories = @categories.to_a
@categories.each do |c|
topics_in_cat = @topics_by_category_id[c.id] if latest_post_only?
if topics_in_cat.present? @all_topics = []
c.displayable_topics = [] @categories.each do |c|
topics_in_cat.each do |topic_id| if c.latest_post && c.latest_post.topic
topic = @topics_by_id[topic_id] c.displayable_topics = [c.latest_post.topic]
if topic.present? topic = c.latest_post.topic
topic.category = c topic.include_last_poster = true # hint for serialization
c.displayable_topics << topic @all_topics << topic
end
end
end
if @topics_by_category_id
@categories.each do |c|
topics_in_cat = @topics_by_category_id[c.id]
if topics_in_cat.present?
c.displayable_topics = []
topics_in_cat.each do |topic_id|
topic = @topics_by_id[topic_id]
if topic.present?
topic.category = c
c.displayable_topics << topic
end
end end
end end
end end

View File

@@ -84,6 +84,9 @@ class Post < ActiveRecord::Base
super super
update_flagged_posts_count update_flagged_posts_count
TopicLink.extract_from(self) TopicLink.extract_from(self)
if topic && topic.category_id
topic.category.update_latest
end
end end
# The key we use in redis to ensure unique posts # The key we use in redis to ensure unique posts

View File

@@ -98,6 +98,7 @@ class Topic < ActiveRecord::Base
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 attr_accessor :topic_list
attr_accessor :include_last_poster
# The regular order # The regular order
scope :topic_list_order, lambda { order('topics.bumped_at desc') } scope :topic_list_order, lambda { order('topics.bumped_at desc') }

View File

@@ -6,6 +6,7 @@ class CategoryDetailedSerializer < ApplicationSerializer
:text_color, :text_color,
:slug, :slug,
:topic_count, :topic_count,
:post_count,
:topics_week, :topics_week,
:topics_month, :topics_month,
:topics_year, :topics_year,

View File

@@ -19,6 +19,12 @@ class ListableTopicSerializer < BasicTopicSerializer
:closed, :closed,
:archived :archived
has_one :last_poster, serializer: BasicUserSerializer, embed: :objects
def include_associations!
include! :last_poster if object.include_last_poster
end
def bumped def bumped
object.created_at < object.bumped_at object.created_at < object.bumped_at
end end

View File

@@ -189,6 +189,7 @@ en:
posts: "Posts" posts: "Posts"
topics: "Topics" topics: "Topics"
latest: "Latest" latest: "Latest"
by: "by"
user: user:
said: "{{username}} said:" said: "{{username}} said:"
@@ -591,6 +592,12 @@ en:
private_message: 'Start a private message' private_message: 'Start a private message'
list: 'Topics' list: 'Topics'
new: 'new topic' new: 'new topic'
new_topics:
one: '1 new topic'
other: '{{count}} new topics'
unread_topics:
one: '1 unread topic'
other: '{{count}} unread topics'
title: 'Topic' title: 'Topic'
loading_more: "Loading more Topics..." loading_more: "Loading more Topics..."
loading: 'Loading topic...' loading: 'Loading topic...'

View File

@@ -0,0 +1,33 @@
class AddLatestToCategories < ActiveRecord::Migration
def up
add_column :categories, :latest_post_id, :integer
add_column :categories, :latest_topic_id, :integer
execute <<SQL
UPDATE categories c
SET latest_post_id = x.post_id
FROM (select category_id, max(p.id) post_id FROM posts p
JOIN topics t on t.id = p.topic_id
WHERE p.deleted_at IS NULL AND NOT p.hidden AND t.visible
GROUP BY category_id
) x
WHERE x.category_id = c.id
SQL
execute <<SQL
UPDATE categories c
SET latest_topic_id = x.topic_id
FROM (select category_id, max(t.id) topic_id
FROM topics t
WHERE t.deleted_at IS NULL AND t.visible
GROUP BY category_id
) x
WHERE x.category_id = c.id
SQL
end
def down
remove_column :categories, :latest_post_id
remove_column :categories, :latest_topic_id
end
end

View File

@@ -80,10 +80,13 @@ class PostCreator
SpamRulesEnforcer.enforce!(@post) SpamRulesEnforcer.enforce!(@post)
end end
track_latest_on_category
enqueue_jobs enqueue_jobs
@post @post
end end
def self.create(user, opts) def self.create(user, opts)
PostCreator.new(user, opts).create PostCreator.new(user, opts).create
end end
@@ -107,6 +110,15 @@ class PostCreator
protected protected
def track_latest_on_category
if @post && @post.errors.count == 0 && @topic && @topic.category_id
Category.update_all( {latest_post_id: @post.id}, {id: @topic.category_id} )
if @post.post_number == 1
Category.update_all( {latest_topic_id: @topic.id}, {id: @topic.category_id} )
end
end
end
def ensure_in_allowed_users def ensure_in_allowed_users
return unless @topic.private_message? return unless @topic.private_message?

View File

@@ -98,6 +98,15 @@ class PostDestroyer
Notification.delete_all topic_id: @post.topic_id, post_number: @post.post_number Notification.delete_all topic_id: @post.topic_id, post_number: @post.post_number
@post.topic.trash!(@user) if @post.topic and @post.post_number == 1 @post.topic.trash!(@user) if @post.topic and @post.post_number == 1
if @post.topic && @post.topic.category && @post.id == @post.topic.category.latest_post_id
@post.topic.category.update_latest
end
if @post.post_number == 1 && @post.topic && @post.topic.category && @post.topic_id == @post.topic.category.latest_topic_id
@post.topic.category.update_latest
end
end end
end end

View File

@@ -1,6 +1,7 @@
# encoding: utf-8 # encoding: utf-8
require 'spec_helper' require 'spec_helper'
require_dependency 'post_creator'
describe Category do describe Category do
it { should validate_presence_of :user_id } it { should validate_presence_of :user_id }
@@ -248,6 +249,31 @@ describe Category do
end end
end end
describe 'latest' do
it 'should be updated correctly' do
category = Fabricate(:category)
post = create_post(category: category.name)
category.reload
category.latest_post_id.should == post.id
category.latest_topic_id.should == post.topic_id
post2 = create_post(category: category.name)
post3 = create_post(topic_id: post.topic_id, category: category.name)
category.reload
category.latest_post_id.should == post3.id
category.latest_topic_id.should == post2.topic_id
destroyer = PostDestroyer.new(Fabricate(:admin), post3)
destroyer.destroy
category.reload
category.latest_post_id.should == post2.id
end
end
describe 'update_stats' do describe 'update_stats' do
before do before do
@category = Fabricate(:category) @category = Fabricate(:category)