diff --git a/app/assets/javascripts/discourse/models/category.js b/app/assets/javascripts/discourse/models/category.js index 7ce772e6195..fb944c4f344 100644 --- a/app/assets/javascripts/discourse/models/category.js +++ b/app/assets/javascripts/discourse/models/category.js @@ -134,7 +134,34 @@ Discourse.Category = Discourse.Model.extend({ newTopics: function(){ 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')]; + } + } }); diff --git a/app/assets/javascripts/discourse/templates/list/wide_categories.js.handlebars b/app/assets/javascripts/discourse/templates/list/wide_categories.js.handlebars index 3135804c155..be2be9e8481 100644 --- a/app/assets/javascripts/discourse/templates/list/wide_categories.js.handlebars +++ b/app/assets/javascripts/discourse/templates/list/wide_categories.js.handlebars @@ -5,8 +5,8 @@ {{i18n categories.category}} {{i18n categories.latest}} - {{i18n categories.topics}} - {{i18n categories.posts}} + {{i18n categories.topics}} + {{i18n categories.posts}} {{#if canEdit}} {{/if}} @@ -79,8 +79,16 @@ {{/each}} - {{number topic_count}} - {{number post_count}} + + {{#each stat in topicCountStatsStrings}} + {{stat}}
+ {{/each}} + + + {{#each stat in postCountStatsStrings}} + {{stat}}
+ {{/each}} + {{/each}} diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss index c6cd133a82f..d77d2acddce 100644 --- a/app/assets/stylesheets/desktop/topic-list.scss +++ b/app/assets/stylesheets/desktop/topic-list.scss @@ -250,14 +250,13 @@ th.num { width: 45px; } + th.stats { + width: 90px; + } .last-user-info { font-size: 12px; } - .has-description td.category { - padding-top: 15px; - } - .has-description { td.category { padding-top: 15px; @@ -266,7 +265,7 @@ .category{ position: relative; - width: 55%; + width: 45%; .subcategories { margin-top: 10px; diff --git a/app/models/category.rb b/app/models/category.rb index 512070ab299..bbd9ee6556f 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -105,7 +105,7 @@ class Category < ActiveRecord::Base 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. def self.update_stats topics = Topic @@ -135,11 +135,62 @@ class Category < ActiveRecord::Base 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 Category.update_all("topics_year = (#{topics_year}), 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 # Internal: Generate the text of post prompting to enter category diff --git a/app/models/post.rb b/app/models/post.rb index 6b0692e47c8..8c9e3d11fba 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -47,6 +47,7 @@ class Post < ActiveRecord::Base scope :by_newest, -> { order('created_at desc, id desc') } scope :by_post_number, -> { order('post_number ASC') } 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 :private_posts, -> { joins(:topic).where('topics.archetype = ?', Archetype.private_message) } scope :with_topic_subtype, ->(subtype) { joins(:topic).where('topics.subtype = ?', subtype) } diff --git a/app/serializers/category_detailed_serializer.rb b/app/serializers/category_detailed_serializer.rb index 0065e2049eb..8551255eaa7 100644 --- a/app/serializers/category_detailed_serializer.rb +++ b/app/serializers/category_detailed_serializer.rb @@ -1,9 +1,15 @@ class CategoryDetailedSerializer < BasicCategorySerializer - attributes :post_count, + attributes :topic_count, + :post_count, + :topics_day, :topics_week, :topics_month, :topics_year, + :posts_day, + :posts_week, + :posts_month, + :posts_year, :description_excerpt, :is_uncategorized, :subcategory_ids @@ -11,6 +17,7 @@ class CategoryDetailedSerializer < BasicCategorySerializer has_many :featured_users, serializer: BasicUserSerializer has_many :displayable_topics, serializer: ListableTopicSerializer, embed: :objects, key: :topics + def topics_week object.topics_week || 0 end @@ -23,6 +30,18 @@ class CategoryDetailedSerializer < BasicCategorySerializer object.topics_year || 0 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 object.id == SiteSetting.uncategorized_category_id end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index c8bc2651455..af37bf5adf7 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -193,6 +193,8 @@ en: latest_by: "latest by" toggle_ordering: "toggle ordering control" subcategories: "Subcategories:" + total_topics: "Total topics: %{count}" + total_posts: "Total posts: %{count}" user: said: "{{username}} said:" @@ -375,6 +377,7 @@ en: month_desc: 'topics posted in the last 30 days' week: 'week' week_desc: 'topics posted in the last 7 days' + day: 'day' first_post: First post mute: Mute diff --git a/db/migrate/20131212225511_add_post_count_stats_columns_to_categories.rb b/db/migrate/20131212225511_add_post_count_stats_columns_to_categories.rb new file mode 100644 index 00000000000..4c42074a2dd --- /dev/null +++ b/db/migrate/20131212225511_add_post_count_stats_columns_to_categories.rb @@ -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 diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb index 24d93225953..4ed827ce185 100644 --- a/spec/models/category_spec.rb +++ b/spec/models/category_spec.rb @@ -294,6 +294,9 @@ describe Category do @category.topics_year.should == 1 @category.topic_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 @@ -312,8 +315,29 @@ describe Category do @category.topics_month.should == 0 @category.topics_year.should == 0 @category.post_count.should == 0 + @category.posts_year.should == 0 + @category.posts_month.should == 0 + @category.posts_week.should == 0 + 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 diff --git a/test/javascripts/models/category_test.js b/test/javascripts/models/category_test.js index 269d9039bee..6c9064d3ff4 100644 --- a/test/javascripts/models/category_test.js +++ b/test/javascripts/models/category_test.js @@ -37,4 +37,31 @@ test('findBySlug', function() { equal(Discourse.Category.findBySlug('luke', 'darth'), luke, 'we can find a child with 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'); -}); \ No newline at end of file +}); + +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"); +});