Show topic and post counts by day/week/month/year on categories page
This commit is contained in:
parent
a7a7387da1
commit
49c3482464
|
@ -134,7 +134,34 @@ Discourse.Category = Discourse.Model.extend({
|
||||||
|
|
||||||
newTopics: function(){
|
newTopics: function(){
|
||||||
return this.get('topicTrackingState').countNew(this.get('name'));
|
return this.get('topicTrackingState').countNew(this.get('name'));
|
||||||
}.property('topicTrackingState.messageCount')
|
}.property('topicTrackingState.messageCount'),
|
||||||
|
|
||||||
|
totalTopicsTitle: function() {
|
||||||
|
return I18n.t('categories.total_topics', {count: this.get('topic_count')});
|
||||||
|
}.property('post_count'),
|
||||||
|
|
||||||
|
totalPostsTitle: function() {
|
||||||
|
return I18n.t('categories.total_posts', {count: this.get('post_count')});
|
||||||
|
}.property('post_count'),
|
||||||
|
|
||||||
|
topicCountStatsStrings: function() {
|
||||||
|
return this.countStatsStrings('topics');
|
||||||
|
}.property('posts_year', 'posts_month', 'posts_week', 'posts_day'),
|
||||||
|
|
||||||
|
postCountStatsStrings: function() {
|
||||||
|
return this.countStatsStrings('posts');
|
||||||
|
}.property('posts_year', 'posts_month', 'posts_week', 'posts_day'),
|
||||||
|
|
||||||
|
countStatsStrings: function(prefix) {
|
||||||
|
var sep = ' / ';
|
||||||
|
if (this.get(prefix + '_day') > 1) {
|
||||||
|
return [this.get(prefix + '_day') + sep + I18n.t('day'), this.get(prefix + '_week') + sep + I18n.t('week')];
|
||||||
|
} else if (this.get(prefix + '_week') > 1) {
|
||||||
|
return [this.get(prefix + '_week') + sep + I18n.t('week'), this.get(prefix + '_month') + sep + I18n.t('month')];
|
||||||
|
} else {
|
||||||
|
return [this.get(prefix + '_month') + sep + I18n.t('month'), this.get(prefix + '_year') + sep + I18n.t('year')];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th class='category'>{{i18n categories.category}}</th>
|
<th class='category'>{{i18n categories.category}}</th>
|
||||||
<th class='latest'>{{i18n categories.latest}}</th>
|
<th class='latest'>{{i18n categories.latest}}</th>
|
||||||
<th class='num topics'>{{i18n categories.topics}}</th>
|
<th class='stats topics'>{{i18n categories.topics}}</th>
|
||||||
<th class='num posts'>{{i18n categories.posts}}
|
<th class='stats posts'>{{i18n categories.posts}}
|
||||||
{{#if canEdit}}
|
{{#if canEdit}}
|
||||||
<button title='{{i18n categories.toggle_ordering}}' class='btn toggle-admin no-text' {{action toggleOrdering}}><i class='fa fa-wrench'></i></button>
|
<button title='{{i18n categories.toggle_ordering}}' class='btn toggle-admin no-text' {{action toggleOrdering}}><i class='fa fa-wrench'></i></button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -79,8 +79,16 @@
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</td>
|
</td>
|
||||||
<td class='num'>{{number topic_count}}</td>
|
<td class='stats' {{bindAttr title="totalTopicsTitle"}}>
|
||||||
<td class='num'>{{number post_count}}</td>
|
{{#each stat in topicCountStatsStrings}}
|
||||||
|
{{stat}}<br/>
|
||||||
|
{{/each}}
|
||||||
|
</td>
|
||||||
|
<td class='stats' {{bindAttr title="totalPostsTitle"}}>
|
||||||
|
{{#each stat in postCountStatsStrings}}
|
||||||
|
{{stat}}<br/>
|
||||||
|
{{/each}}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -250,14 +250,13 @@
|
||||||
th.num {
|
th.num {
|
||||||
width: 45px;
|
width: 45px;
|
||||||
}
|
}
|
||||||
|
th.stats {
|
||||||
|
width: 90px;
|
||||||
|
}
|
||||||
.last-user-info {
|
.last-user-info {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-description td.category {
|
|
||||||
padding-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.has-description {
|
.has-description {
|
||||||
td.category {
|
td.category {
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
|
@ -266,7 +265,7 @@
|
||||||
|
|
||||||
.category{
|
.category{
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 55%;
|
width: 45%;
|
||||||
|
|
||||||
.subcategories {
|
.subcategories {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|
|
@ -105,7 +105,7 @@ class Category < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Internal: Update category stats: # of topics in past year, month, week for
|
# Internal: Update category stats: # of topics and posts in past year, month, week for
|
||||||
# all categories.
|
# all categories.
|
||||||
def self.update_stats
|
def self.update_stats
|
||||||
topics = Topic
|
topics = Topic
|
||||||
|
@ -135,11 +135,62 @@ class Category < ActiveRecord::Base
|
||||||
|
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
|
posts = Post.select("count(*) post_count")
|
||||||
|
.joins(:topic)
|
||||||
|
.where('topics.category_id = categories.id')
|
||||||
|
.where('topics.visible = true')
|
||||||
|
.where("topics.id NOT IN (select cc.topic_id from categories cc WHERE topic_id IS NOT NULL)")
|
||||||
|
.where('posts.deleted_at IS NULL')
|
||||||
|
.where('posts.user_deleted = false')
|
||||||
|
|
||||||
|
posts_year = posts.created_since(1.year.ago).to_sql
|
||||||
|
posts_month = posts.created_since(1.month.ago).to_sql
|
||||||
|
posts_week = posts.created_since(1.week.ago).to_sql
|
||||||
|
|
||||||
# TODO don't update unchanged data
|
# TODO don't update unchanged data
|
||||||
Category.update_all("topics_year = (#{topics_year}),
|
Category.update_all("topics_year = (#{topics_year}),
|
||||||
topics_month = (#{topics_month}),
|
topics_month = (#{topics_month}),
|
||||||
topics_week = (#{topics_week})")
|
topics_week = (#{topics_week}),
|
||||||
|
posts_year = (#{posts_year}),
|
||||||
|
posts_month = (#{posts_month}),
|
||||||
|
posts_week = (#{posts_week})")
|
||||||
|
end
|
||||||
|
|
||||||
|
def visible_posts
|
||||||
|
query = Post.joins(:topic)
|
||||||
|
.where(['topics.category_id = ?', self.id])
|
||||||
|
.where('topics.visible = true')
|
||||||
|
.where('posts.deleted_at IS NULL')
|
||||||
|
.where('posts.user_deleted = false')
|
||||||
|
self.topic_id ? query.where(['topics.id <> ?', self.topic_id]) : query
|
||||||
|
end
|
||||||
|
|
||||||
|
def topics_day
|
||||||
|
if val = $redis.get(topics_day_key)
|
||||||
|
val.to_i
|
||||||
|
else
|
||||||
|
val = self.topics.where(['topics.id <> ?', self.topic_id]).created_since(1.day.ago).visible.count
|
||||||
|
$redis.setex topics_day_key, 30.minutes.to_i, val
|
||||||
|
val
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def topics_day_key
|
||||||
|
"topics_day:cat-#{self.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def posts_day
|
||||||
|
if val = $redis.get(posts_day_key)
|
||||||
|
val.to_i
|
||||||
|
else
|
||||||
|
val = self.visible_posts.created_since(1.day.ago).count
|
||||||
|
$redis.setex posts_day_key, 30.minutes.to_i, val
|
||||||
|
val
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def posts_day_key
|
||||||
|
"posts_day:cat-#{self.id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Internal: Generate the text of post prompting to enter category
|
# Internal: Generate the text of post prompting to enter category
|
||||||
|
|
|
@ -47,6 +47,7 @@ class Post < ActiveRecord::Base
|
||||||
scope :by_newest, -> { order('created_at desc, id desc') }
|
scope :by_newest, -> { order('created_at desc, id desc') }
|
||||||
scope :by_post_number, -> { order('post_number ASC') }
|
scope :by_post_number, -> { order('post_number ASC') }
|
||||||
scope :with_user, -> { includes(:user) }
|
scope :with_user, -> { includes(:user) }
|
||||||
|
scope :created_since, lambda { |time_ago| where('posts.created_at > ?', time_ago) }
|
||||||
scope :public_posts, -> { joins(:topic).where('topics.archetype <> ?', Archetype.private_message) }
|
scope :public_posts, -> { joins(:topic).where('topics.archetype <> ?', Archetype.private_message) }
|
||||||
scope :private_posts, -> { joins(:topic).where('topics.archetype = ?', Archetype.private_message) }
|
scope :private_posts, -> { joins(:topic).where('topics.archetype = ?', Archetype.private_message) }
|
||||||
scope :with_topic_subtype, ->(subtype) { joins(:topic).where('topics.subtype = ?', subtype) }
|
scope :with_topic_subtype, ->(subtype) { joins(:topic).where('topics.subtype = ?', subtype) }
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
class CategoryDetailedSerializer < BasicCategorySerializer
|
class CategoryDetailedSerializer < BasicCategorySerializer
|
||||||
|
|
||||||
attributes :post_count,
|
attributes :topic_count,
|
||||||
|
:post_count,
|
||||||
|
:topics_day,
|
||||||
:topics_week,
|
:topics_week,
|
||||||
:topics_month,
|
:topics_month,
|
||||||
:topics_year,
|
:topics_year,
|
||||||
|
:posts_day,
|
||||||
|
:posts_week,
|
||||||
|
:posts_month,
|
||||||
|
:posts_year,
|
||||||
:description_excerpt,
|
:description_excerpt,
|
||||||
:is_uncategorized,
|
:is_uncategorized,
|
||||||
:subcategory_ids
|
:subcategory_ids
|
||||||
|
@ -11,6 +17,7 @@ class CategoryDetailedSerializer < BasicCategorySerializer
|
||||||
has_many :featured_users, serializer: BasicUserSerializer
|
has_many :featured_users, serializer: BasicUserSerializer
|
||||||
has_many :displayable_topics, serializer: ListableTopicSerializer, embed: :objects, key: :topics
|
has_many :displayable_topics, serializer: ListableTopicSerializer, embed: :objects, key: :topics
|
||||||
|
|
||||||
|
|
||||||
def topics_week
|
def topics_week
|
||||||
object.topics_week || 0
|
object.topics_week || 0
|
||||||
end
|
end
|
||||||
|
@ -23,6 +30,18 @@ class CategoryDetailedSerializer < BasicCategorySerializer
|
||||||
object.topics_year || 0
|
object.topics_year || 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def posts_week
|
||||||
|
object.posts_week || 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def posts_month
|
||||||
|
object.posts_month || 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def posts_year
|
||||||
|
object.posts_year || 0
|
||||||
|
end
|
||||||
|
|
||||||
def is_uncategorized
|
def is_uncategorized
|
||||||
object.id == SiteSetting.uncategorized_category_id
|
object.id == SiteSetting.uncategorized_category_id
|
||||||
end
|
end
|
||||||
|
|
|
@ -193,6 +193,8 @@ en:
|
||||||
latest_by: "latest by"
|
latest_by: "latest by"
|
||||||
toggle_ordering: "toggle ordering control"
|
toggle_ordering: "toggle ordering control"
|
||||||
subcategories: "Subcategories:"
|
subcategories: "Subcategories:"
|
||||||
|
total_topics: "Total topics: %{count}"
|
||||||
|
total_posts: "Total posts: %{count}"
|
||||||
|
|
||||||
user:
|
user:
|
||||||
said: "{{username}} said:"
|
said: "{{username}} said:"
|
||||||
|
@ -375,6 +377,7 @@ en:
|
||||||
month_desc: 'topics posted in the last 30 days'
|
month_desc: 'topics posted in the last 30 days'
|
||||||
week: 'week'
|
week: 'week'
|
||||||
week_desc: 'topics posted in the last 7 days'
|
week_desc: 'topics posted in the last 7 days'
|
||||||
|
day: 'day'
|
||||||
|
|
||||||
first_post: First post
|
first_post: First post
|
||||||
mute: Mute
|
mute: Mute
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
class AddPostCountStatsColumnsToCategories < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
change_table :categories do |t|
|
||||||
|
t.integer :posts_year
|
||||||
|
t.integer :posts_month
|
||||||
|
t.integer :posts_week
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -294,6 +294,9 @@ describe Category do
|
||||||
@category.topics_year.should == 1
|
@category.topics_year.should == 1
|
||||||
@category.topic_count.should == 1
|
@category.topic_count.should == 1
|
||||||
@category.post_count.should == 1
|
@category.post_count.should == 1
|
||||||
|
@category.posts_year.should == 1
|
||||||
|
@category.posts_month.should == 1
|
||||||
|
@category.posts_week.should == 1
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -312,8 +315,29 @@ describe Category do
|
||||||
@category.topics_month.should == 0
|
@category.topics_month.should == 0
|
||||||
@category.topics_year.should == 0
|
@category.topics_year.should == 0
|
||||||
@category.post_count.should == 0
|
@category.post_count.should == 0
|
||||||
|
@category.posts_year.should == 0
|
||||||
|
@category.posts_month.should == 0
|
||||||
|
@category.posts_week.should == 0
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with revised post' do
|
||||||
|
before do
|
||||||
|
post = create_post(user: @category.user, category: @category.name)
|
||||||
|
|
||||||
|
SiteSetting.stubs(:ninja_edit_window).returns(1.minute.to_i)
|
||||||
|
post.revise(post.user, 'updated body', revised_at: post.updated_at + 2.minutes)
|
||||||
|
|
||||||
|
Category.update_stats
|
||||||
|
@category.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't count each version of a post" do
|
||||||
|
@category.post_count.should == 1
|
||||||
|
@category.posts_year.should == 1
|
||||||
|
@category.posts_month.should == 1
|
||||||
|
@category.posts_week.should == 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -38,3 +38,30 @@ test('findBySlug', function() {
|
||||||
blank(Discourse.Category.findBySlug('luke'), 'luke is blank without the parent');
|
blank(Discourse.Category.findBySlug('luke'), 'luke is blank without the parent');
|
||||||
blank(Discourse.Category.findBySlug('luke', 'leia'), 'luke is blank with an incorrect parent');
|
blank(Discourse.Category.findBySlug('luke', 'leia'), 'luke is blank with an incorrect parent');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('postCountStatsStrings', function() {
|
||||||
|
var category1 = Discourse.Category.create({id: 1, slug: 'unloved', posts_year: 2, posts_month: 0, posts_week: 0, posts_day: 0}),
|
||||||
|
category2 = Discourse.Category.create({id: 2, slug: 'hasbeen', posts_year: 50, posts_month: 4, posts_week: 0, posts_day: 0}),
|
||||||
|
category3 = Discourse.Category.create({id: 3, slug: 'solastweek', posts_year: 250, posts_month: 200, posts_week: 50, posts_day: 0}),
|
||||||
|
category4 = Discourse.Category.create({id: 4, slug: 'hotstuff', posts_year: 500, posts_month: 280, posts_week: 100, posts_day: 22});
|
||||||
|
|
||||||
|
var result = category1.get('postCountStatsStrings');
|
||||||
|
equal(result.length, 2, "should show month and year");
|
||||||
|
equal(result[0], '0 / month', "should show month and year");
|
||||||
|
equal(result[1], '2 / year', "should show month and year");
|
||||||
|
|
||||||
|
result = category2.get('postCountStatsStrings');
|
||||||
|
equal(result.length, 2, "should show month and year");
|
||||||
|
equal(result[0], '4 / month', "should show month and year");
|
||||||
|
equal(result[1], '50 / year', "should show month and year");
|
||||||
|
|
||||||
|
result = category3.get('postCountStatsStrings');
|
||||||
|
equal(result.length, 2, "should show week and month");
|
||||||
|
equal(result[0], '50 / week', "should show week and month");
|
||||||
|
equal(result[1], '200 / month', "should show week and month");
|
||||||
|
|
||||||
|
result = category4.get('postCountStatsStrings');
|
||||||
|
equal(result.length, 2, "should show day and week");
|
||||||
|
equal(result[0], '22 / day', "should show day and week");
|
||||||
|
equal(result[1], '100 / week', "should show day and week");
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue