From f18098fd9bc3fc595dbbd33868a18511ec5d217a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr>
Date: Wed, 24 Jun 2015 15:19:39 +0200
Subject: [PATCH] FEATURE: category dropdown in admin reports

---
 .../admin/controllers/admin-reports.js.es6    | 22 ++++----
 .../javascripts/admin/models/report.js.es6    | 23 ++++----
 .../javascripts/admin/templates/reports.hbs   |  1 +
 app/controllers/admin/reports_controller.rb   |  7 ++-
 app/models/post.rb                            |  7 +--
 app/models/post_action.rb                     | 27 ++++++----
 app/models/report.rb                          | 53 +++++++++++--------
 app/models/topic.rb                           | 45 +++++++++-------
 8 files changed, 108 insertions(+), 77 deletions(-)

diff --git a/app/assets/javascripts/admin/controllers/admin-reports.js.es6 b/app/assets/javascripts/admin/controllers/admin-reports.js.es6
index 6bb36989b..0430e8a18 100644
--- a/app/assets/javascripts/admin/controllers/admin-reports.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-reports.js.es6
@@ -4,24 +4,26 @@ export default Ember.ObjectController.extend({
   viewingBarChart: Em.computed.equal('viewMode', 'barChart'),
   startDate: null,
   endDate: null,
+  categoryId: null,
   refreshing: false,
 
   actions: {
-    refreshReport: function() {
-      var self = this;
-      this.set('refreshing', true);
-      Discourse.Report.find(this.get('type'), this.get('startDate'), this.get('endDate')).then(function(r) {
-        self.set('model', r);
-      }).finally(function() {
-        self.set('refreshing', false);
-      });
+    refreshReport() {
+      this.set("refreshing", true);
+      Discourse.Report.find(
+        this.get("type"),
+        this.get("startDate"),
+        this.get("endDate"),
+        this.get("categoryId")
+      ).then(m => this.set("model", m)
+      ).finally(() => this.set("refreshing", false));
     },
 
-    viewAsTable: function() {
+    viewAsTable() {
       this.set('viewMode', 'table');
     },
 
-    viewAsBarChart: function() {
+    viewAsBarChart() {
       this.set('viewMode', 'barChart');
     }
   }
diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6
index d03e6d605..ebb63486d 100644
--- a/app/assets/javascripts/admin/models/report.js.es6
+++ b/app/assets/javascripts/admin/models/report.js.es6
@@ -142,23 +142,24 @@ const Report = Discourse.Model.extend({
 });
 
 Report.reopenClass({
-  find: function(type, startDate, endDate) {
 
-    return Discourse.ajax("/admin/reports/" + type, {data: {
-      start_date: startDate,
-      end_date: endDate
-    }}).then(function (json) {
+  find(type, startDate, endDate, categoryId) {
+    return Discourse.ajax("/admin/reports/" + type, {
+      data: {
+        start_date: startDate,
+        end_date: endDate,
+        category_id: categoryId
+      }
+    }).then(json => {
       // Add a percent field to each tuple
-      var maxY = 0;
-      json.report.data.forEach(function (row) {
+      let maxY = 0;
+      json.report.data.forEach(row => {
         if (row.y > maxY) maxY = row.y;
       });
       if (maxY > 0) {
-        json.report.data.forEach(function (row) {
-          row.percentage = Math.round((row.y / maxY) * 100);
-        });
+        json.report.data.forEach(row => row.percentage = Math.round((row.y / maxY) * 100));
       }
-      var model = Discourse.Report.create({type: type});
+      const model = Discourse.Report.create({ type: type });
       model.setProperties(json.report);
       return model;
     });
diff --git a/app/assets/javascripts/admin/templates/reports.hbs b/app/assets/javascripts/admin/templates/reports.hbs
index 0bc26a873..2bbc04b18 100644
--- a/app/assets/javascripts/admin/templates/reports.hbs
+++ b/app/assets/javascripts/admin/templates/reports.hbs
@@ -3,6 +3,7 @@
 <div>
   {{i18n 'admin.dashboard.reports.start_date'}} {{input type="date" value=startDate}}
   {{i18n 'admin.dashboard.reports.end_date'}} {{input type="date" value=endDate}}
+  {{category-chooser valueAttribute="id" value=categoryId source=categoryId}}
   {{d-button action="refreshReport" class="btn-primary" label="admin.dashboard.reports.refresh_report" icon="refresh"}}
 </div>
 
diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb
index cd9b7c854..e4416b130 100644
--- a/app/controllers/admin/reports_controller.rb
+++ b/app/controllers/admin/reports_controller.rb
@@ -13,7 +13,12 @@ class Admin::ReportsController < Admin::AdminController
     end_date = start_date + 1.month
     end_date = Time.parse(params[:end_date]) if params[:end_date].present?
 
-    report = Report.find(report_type, {start_date: start_date, end_date: end_date})
+    category_id = if params.has_key?(:category_id)
+      params[:category_id].blank? ? SiteSetting.uncategorized_category_id : params[:category_id].to_i
+    end
+
+    report = Report.find(report_type, start_date: start_date, end_date: end_date, category_id: category_id)
+
     raise Discourse::NotFound if report.blank?
 
     render_json_dump(report: report)
diff --git a/app/models/post.rb b/app/models/post.rb
index 0dbe2c6c3..22fbea3ec 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -493,15 +493,16 @@ class Post < ActiveRecord::Base
     Jobs.enqueue(:process_post, args)
   end
 
-  def self.public_posts_count_per_day(start_date, end_date)
-    public_posts.where('posts.created_at >= ? AND posts.created_at <= ?', start_date, end_date).group('date(posts.created_at)').order('date(posts.created_at)').count
+  def self.public_posts_count_per_day(start_date, end_date, category_id=nil)
+    result = public_posts.where('posts.created_at >= ? AND posts.created_at <= ?', start_date, end_date)
+    result = result.where('topics.category_id = ?', category_id) if category_id
+    result.group('date(posts.created_at)').order('date(posts.created_at)').count
   end
 
   def self.private_messages_count_per_day(since_days_ago, topic_subtype)
     private_posts.with_topic_subtype(topic_subtype).where('posts.created_at > ?', since_days_ago.days.ago).group('date(posts.created_at)').order('date(posts.created_at)').count
   end
 
-
   def reply_history(max_replies=100)
     post_ids = Post.exec_sql("WITH RECURSIVE breadcrumb(id, reply_to_post_number) AS (
                               SELECT p.id, p.reply_to_post_number FROM posts AS p
diff --git a/app/models/post_action.rb b/app/models/post_action.rb
index 5fc133012..f1746196a 100644
--- a/app/models/post_action.rb
+++ b/app/models/post_action.rb
@@ -39,11 +39,13 @@ class PostAction < ActiveRecord::Base
     nil
   end
 
-  def self.flag_count_by_date(start_date, end_date)
-    where('created_at >= ? and created_at <= ?', start_date, end_date)
-      .where(post_action_type_id: PostActionType.flag_types.values)
-      .group('date(created_at)').order('date(created_at)')
-      .count
+  def self.flag_count_by_date(start_date, end_date, category_id=nil)
+    result = where('post_actions.created_at >= ? AND post_actions.created_at <= ?', start_date, end_date)
+    result = result.where(post_action_type_id: PostActionType.flag_types.values)
+    result = result.joins(post: :topic).where("topics.category_id = ?", category_id) if category_id
+    result.group('date(post_actions.created_at)')
+          .order('date(post_actions.created_at)')
+          .count
   end
 
   def self.update_flagged_posts_count
@@ -122,12 +124,15 @@ SQL
     user_actions
   end
 
-  def self.count_per_day_for_type(post_action_type, since_days_ago=30)
-    unscoped.where(post_action_type_id: post_action_type)
-            .where('created_at >= ?', since_days_ago.days.ago)
-            .group('date(created_at)')
-            .order('date(created_at)')
-            .count
+  def self.count_per_day_for_type(post_action_type, opts=nil)
+    opts ||= {}
+    opts[:since_days_ago] ||= 30
+    result = unscoped.where(post_action_type_id: post_action_type)
+    result = result.where('post_actions.created_at >= ?', opts[:since_days_ago].days.ago)
+    result = result.joins(post: :topic).where('topics.category_id = ?', opts[:category_id]) if opts[:category_id]
+    result.group('date(post_actions.created_at)')
+          .order('date(post_actions.created_at)')
+          .count
   end
 
   def self.agree_flags!(post, moderator, delete_post=false)
diff --git a/app/models/report.rb b/app/models/report.rb
index 5c6c9ad9d..d97a97057 100644
--- a/app/models/report.rb
+++ b/app/models/report.rb
@@ -2,7 +2,7 @@ require_dependency 'topic_subtype'
 
 class Report
 
-  attr_accessor :type, :data, :total, :prev30Days, :start_date, :end_date
+  attr_accessor :type, :data, :total, :prev30Days, :start_date, :end_date, :category_id
 
   def self.default_days
     30
@@ -10,14 +10,11 @@ class Report
 
   def initialize(type)
     @type = type
-    @data = nil
-    @total = nil
-    @prev30Days = nil
     @start_date ||= 1.month.ago.beginning_of_day
     @end_date ||= Time.zone.now.end_of_day
   end
 
-  def as_json(options = nil)
+  def as_json(options=nil)
     {
      type: type,
      title: I18n.t("reports.#{type}.title"),
@@ -27,6 +24,7 @@ class Report
      total: total,
      start_date: start_date,
      end_date: end_date,
+     category_id: category_id,
      prev30Days: self.prev30Days
     }
   end
@@ -37,6 +35,7 @@ class Report
     report = Report.new(type)
     report.start_date = opts[:start_date] if opts[:start_date]
     report.end_date = opts[:end_date] if opts[:end_date]
+    report.category_id = opts[:category_id] if opts[:category_id]
     report_method = :"report_#{type}"
 
     if respond_to?(report_method)
@@ -46,6 +45,7 @@ class Report
     else
       return nil
     end
+
     report
   end
 
@@ -60,13 +60,14 @@ class Report
       end
 
     filtered_results = data.where('date >= ? AND date <= ?', report.start_date.to_date, report.end_date.to_date)
+    filtered_results = filtered_results.where(category_id: report.category_id) if report.category_id
 
     report.data = []
     filtered_results.order(date: :asc)
                     .group(:date)
                     .sum(:count)
                     .each do |date, count|
-      report.data << {x: date, y: count}
+      report.data << { x: date, y: count }
     end
 
     report.total      = data.sum(:count)
@@ -84,28 +85,32 @@ class Report
   end
 
   def self.report_topics(report)
-    basic_report_about report, Topic, :listable_count_per_day, report.start_date, report.end_date
-    add_counts report, Topic.listable_topics, 'topics.created_at'
+    basic_report_about report, Topic, :listable_count_per_day, report.start_date, report.end_date, report.category_id
+    countable = Topic.listable_topics
+    countable = countable.where(category_id: report.category_id) if report.category_id
+    add_counts report, countable, 'topics.created_at'
   end
 
   def self.report_posts(report)
-    basic_report_about report, Post, :public_posts_count_per_day, report.start_date, report.end_date
-    add_counts report, Post.public_posts, 'posts.created_at'
+    basic_report_about report, Post, :public_posts_count_per_day, report.start_date, report.end_date, report.category_id
+    countable = Post.public_posts
+    countable = countable.joins(:topic).where("topics.category_id = ?", report.category_id) if report.category_id
+    add_counts report, countable, 'posts.created_at'
   end
 
   def self.report_time_to_first_response(report)
     report.data = []
-    Topic.time_to_first_response_per_day(report.start_date, report.end_date).each do |r|
+    Topic.time_to_first_response_per_day(report.start_date, report.end_date, report.category_id).each do |r|
       report.data << { x: Date.parse(r["date"]), y: r["hours"].to_f.round(2) }
     end
-    report.total = Topic.time_to_first_response_total
-    report.prev30Days = Topic.time_to_first_response_total(report.start_date - 30.days, report.start_date)
+    report.total = Topic.time_to_first_response_total(category_id: report.category_id)
+    report.prev30Days = Topic.time_to_first_response_total(start_date: report.start_date - 30.days, end_date: report.start_date, category_id: report.category_id)
   end
 
   def self.report_topics_with_no_response(report)
-    basic_report_about report, Topic, :with_no_response_per_day, report.start_date, report.end_date
-    report.total = Topic.with_no_response_total
-    report.prev30Days = Topic.with_no_response_total(report.start_date - 30.days, report.start_date)
+    basic_report_about report, Topic, :with_no_response_per_day, report.start_date, report.end_date, report.category_id
+    report.total = Topic.with_no_response_total(category_id: report.category_id)
+    report.prev30Days = Topic.with_no_response_total(start_date: report.start_date - 30.days, end_date: report.start_date, category_id: report.category_id)
   end
 
   def self.report_emails(report)
@@ -120,7 +125,7 @@ class Report
   def self.basic_report_about(report, subject_class, report_method, *args)
     report.data = []
     subject_class.send(report_method, *args).each do |date, count|
-      report.data << {x: date, y: count}
+      report.data << { x: date, y: count }
     end
   end
 
@@ -132,14 +137,16 @@ class Report
   def self.report_users_by_trust_level(report)
     report.data = []
     User.real.group('trust_level').count.each do |level, count|
-      report.data << {x: level.to_i, y: count}
+      report.data << { x: level.to_i, y: count }
     end
   end
 
   # Post action counts:
   def self.report_flags(report)
-    basic_report_about report, PostAction, :flag_count_by_date, report.start_date, report.end_date
-    add_counts report, PostAction.where(post_action_type_id: PostActionType.flag_types.values), 'post_actions.created_at'
+    basic_report_about report, PostAction, :flag_count_by_date, report.start_date, report.end_date, report.category_id
+    countable = PostAction.where(post_action_type_id: PostActionType.flag_types.values)
+    countable = countable.joins(post: :topic).where("topics.category_id = ?", report.category_id) if report.category_id
+    add_counts report, countable, 'post_actions.created_at'
   end
 
   def self.report_likes(report)
@@ -152,10 +159,12 @@ class Report
 
   def self.post_action_report(report, post_action_type)
     report.data = []
-    PostAction.count_per_day_for_type(post_action_type).each do |date, count|
+    PostAction.count_per_day_for_type(post_action_type, category_id: report.category_id).each do |date, count|
       report.data << { x: date, y: count }
     end
-    add_counts report, PostAction.unscoped.where(post_action_type_id: post_action_type), 'post_actions.created_at'
+    countable = PostAction.unscoped.where(post_action_type_id: post_action_type)
+    countable = countable.joins(post: :topic).where("topics.category_id = ?", report.category_id) if report.category_id
+    add_counts report, countable, 'post_actions.created_at'
   end
 
   # Private messages counts:
diff --git a/app/models/topic.rb b/app/models/topic.rb
index cfdae5d20..1ee7926e7 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -359,8 +359,10 @@ class Topic < ActiveRecord::Base
     custom_fields[key.to_s]
   end
 
-  def self.listable_count_per_day(start_date, end_date)
-    listable_topics.where('created_at >= ? and created_at <= ?', start_date, end_date).group('date(created_at)').order('date(created_at)').count
+  def self.listable_count_per_day(start_date, end_date, category_id=nil)
+    result = listable_topics.where('created_at >= ? and created_at <= ?', start_date, end_date)
+    result = result.where(category_id: category_id) if category_id
+    result.group('date(created_at)').order('date(created_at)').count
   end
 
   def private_message?
@@ -872,10 +874,12 @@ class Topic < ActiveRecord::Base
     ) t
   SQL
 
-  def self.time_to_first_response(sql, start_date=nil, end_date=nil)
+  def self.time_to_first_response(sql, opts=nil)
+    opts ||= {}
     builder = SqlBuilder.new(sql)
-    builder.where("t.created_at >= :start_date", start_date: start_date) if start_date
-    builder.where("t.created_at <= :end_date", end_date: end_date) if end_date
+    builder.where("t.created_at >= :start_date", start_date: opts[:start_date]) if opts[:start_date]
+    builder.where("t.created_at <= :end_date", end_date: opts[:end_date]) if opts[:end_date]
+    builder.where("t.category_id = :category_id", category_id: opts[:category_id]) if opts[:category_id]
     builder.where("t.archetype <> '#{Archetype.private_message}'")
     builder.where("t.deleted_at IS NULL")
     builder.where("p.deleted_at IS NULL")
@@ -884,27 +888,30 @@ class Topic < ActiveRecord::Base
     builder.exec
   end
 
-  def self.time_to_first_response_per_day(start_date, end_date)
-    time_to_first_response(TIME_TO_FIRST_RESPONSE_SQL, start_date, end_date)
+  def self.time_to_first_response_per_day(start_date, end_date, category_id=nil)
+    time_to_first_response(TIME_TO_FIRST_RESPONSE_SQL, start_date: start_date, end_date: end_date, category_id: category_id)
   end
 
-  def self.time_to_first_response_total(start_date=nil, end_date=nil)
-    result = time_to_first_response(TIME_TO_FIRST_RESPONSE_TOTAL_SQL, start_date, end_date)
-    result.first["hours"].to_f.round(2)
+  def self.time_to_first_response_total(opts=nil)
+    total = time_to_first_response(TIME_TO_FIRST_RESPONSE_TOTAL_SQL, opts)
+    total.first["hours"].to_f.round(2)
   end
 
-  def self.with_no_response_per_day(start_date, end_date)
-    listable_topics.where(highest_post_number: 1)
-                   .where("created_at BETWEEN ? AND ?", start_date, end_date)
-                   .group("created_at::date")
-                   .order("created_at::date")
-                   .count
+  def self.with_no_response_per_day(start_date, end_date, category_id=nil)
+    result = listable_topics.where(highest_post_number: 1)
+    result = result.where("created_at BETWEEN ? AND ?", start_date, end_date)
+    result = result.where(category_id: category_id) if category_id
+    result.group("created_at::date")
+          .order("created_at::date")
+          .count
   end
 
-  def self.with_no_response_total(start_date=nil, end_date=nil)
+  def self.with_no_response_total(opts=nil)
+    opts ||= {}
     total = listable_topics.where(highest_post_number: 1)
-    total = total.where("created_at >= ?", start_date) if start_date
-    total = total.where("created_at <= ?", end_date) if end_date
+    total = total.where("created_at >= ?", opts[:start_date]) if opts[:start_date]
+    total = total.where("created_at <= ?", opts[:end_date]) if opts[:end_date]
+    total = total.where(category_id: opts[:category_id]) if opts[:category_id]
     total.count
   end