diff --git a/app/models/incoming_links_report.rb b/app/models/incoming_links_report.rb index 38e58d0e531..8635fcdd90c 100644 --- a/app/models/incoming_links_report.rb +++ b/app/models/incoming_links_report.rb @@ -1,11 +1,12 @@ class IncomingLinksReport - attr_accessor :type, :data, :y_titles, :start_date, :end_date, :limit + attr_accessor :type, :data, :y_titles, :start_date, :end_date, :limit, :category_id def initialize(type) @type = type @y_titles = {} @data = nil + @category_id = nil end def as_json(_options = nil) @@ -30,6 +31,7 @@ class IncomingLinksReport report.start_date = _opts[:start_date] || 30.days.ago report.end_date = _opts[:end_date] || Time.now.end_of_day report.limit = _opts[:limit].to_i if _opts[:limit] + report.category_id = _opts[:category_id] if _opts[:category_id] send(report_method, report) report @@ -40,8 +42,8 @@ class IncomingLinksReport report.y_titles[:num_clicks] = I18n.t("reports.#{report.type}.num_clicks") report.y_titles[:num_topics] = I18n.t("reports.#{report.type}.num_topics") - num_clicks = link_count_per_user(start_date: report.start_date, end_date: report.end_date) - num_topics = topic_count_per_user(start_date: report.start_date, end_date: report.end_date) + num_clicks = link_count_per_user(start_date: report.start_date, end_date: report.end_date, category_id: report.category_id) + num_topics = topic_count_per_user(start_date: report.start_date, end_date: report.end_date, category_id: report.category_id) user_id_lookup = User.where(username: num_clicks.keys).select(:id, :username).inject({}) { |sum, v| sum[v.username] = v.id; sum; } report.data = [] num_clicks.each_key do |username| @@ -50,19 +52,19 @@ class IncomingLinksReport report.data = report.data.sort_by { |x| x[:num_clicks] }.reverse[0, 10] end - def self.per_user(start_date:, end_date:) - @per_user_query ||= public_incoming_links + def self.per_user(start_date:, end_date:, category_id:) + @per_user_query ||= public_incoming_links(category_id: category_id) .where('incoming_links.created_at > ? AND incoming_links.created_at < ? AND incoming_links.user_id IS NOT NULL', start_date, end_date) .joins(:user) .group('users.username') end - def self.link_count_per_user(start_date:, end_date:) - per_user(start_date: start_date, end_date: end_date).count + def self.link_count_per_user(start_date:, end_date:, category_id:) + per_user(start_date: start_date, end_date: end_date, category_id: category_id).count end - def self.topic_count_per_user(start_date:, end_date:) - per_user(start_date: start_date, end_date: end_date).joins(:post).count("DISTINCT posts.topic_id") + def self.topic_count_per_user(start_date:, end_date:, category_id:) + per_user(start_date: start_date, end_date: end_date, category_id: category_id).joins(:post).count("DISTINCT posts.topic_id") end # Return top 10 domains that brought traffic to the site within the last 30 days @@ -71,8 +73,8 @@ class IncomingLinksReport report.y_titles[:num_topics] = I18n.t("reports.#{report.type}.num_topics") report.y_titles[:num_users] = I18n.t("reports.#{report.type}.num_users") - num_clicks = link_count_per_domain(start_date: report.start_date, end_date: report.end_date) - num_topics = topic_count_per_domain(num_clicks.keys) + num_clicks = link_count_per_domain(start_date: report.start_date, end_date: report.end_date, category_id: report.category_id) + num_topics = topic_count_per_domain(num_clicks.keys, category_id: report.category_id) report.data = [] num_clicks.each_key do |domain| report.data << { domain: domain, num_clicks: num_clicks[domain], num_topics: num_topics[domain] } @@ -80,8 +82,8 @@ class IncomingLinksReport report.data = report.data.sort_by { |x| x[:num_clicks] }.reverse[0, 10] end - def self.link_count_per_domain(limit: 10, start_date:, end_date:) - public_incoming_links + def self.link_count_per_domain(limit: 10, start_date:, end_date:, category_id:) + public_incoming_links(category_id: category_id) .where('incoming_links.created_at > ? AND incoming_links.created_at < ?', start_date, end_date) .joins(incoming_referer: :incoming_domain) .group('incoming_domains.name') @@ -90,24 +92,25 @@ class IncomingLinksReport .count end - def self.per_domain(domains) - public_incoming_links + def self.per_domain(domains, options = {}) + public_incoming_links(category_id: options[:category_id]) .joins(incoming_referer: :incoming_domain) .where('incoming_links.created_at > ? AND incoming_domains.name IN (?)', 30.days.ago, domains) .group('incoming_domains.name') end - def self.topic_count_per_domain(domains) + def self.topic_count_per_domain(domains, options = {}) # COUNT(DISTINCT) is slow - per_domain(domains).count("DISTINCT posts.topic_id") + per_domain(domains, options).count("DISTINCT posts.topic_id") end def self.report_top_referred_topics(report) report.y_titles[:num_clicks] = I18n.t("reports.#{report.type}.num_clicks") - num_clicks = link_count_per_topic(start_date: report.start_date, end_date: report.end_date) + num_clicks = link_count_per_topic(start_date: report.start_date, end_date: report.end_date, category_id: report.category_id) num_clicks = num_clicks.to_a.sort_by { |x| x[1] }.last(report.limit || 10).reverse report.data = [] topics = Topic.select('id, slug, title').where('id in (?)', num_clicks.map { |z| z[0] }) + topics = topics.in_category_and_subcategories(report.category_id) if report.category_id num_clicks.each do |topic_id, num_clicks_element| topic = topics.find { |t| t.id == topic_id } if topic @@ -117,16 +120,17 @@ class IncomingLinksReport report.data end - def self.link_count_per_topic(start_date:, end_date:) - public_incoming_links + def self.link_count_per_topic(start_date:, end_date:, category_id:) + public_incoming_links(category_id: category_id) .where('incoming_links.created_at > ? AND incoming_links.created_at < ? AND topic_id IS NOT NULL', start_date, end_date) .group('topic_id') .count end - def self.public_incoming_links + def self.public_incoming_links(category_id: nil) IncomingLink .joins(post: :topic) .where("topics.archetype = ?", Archetype.default) + .merge(Topic.in_category_and_subcategories(category_id)) end end diff --git a/app/models/post_action.rb b/app/models/post_action.rb index 966286d74e3..e4bf8986f7a 100644 --- a/app/models/post_action.rb +++ b/app/models/post_action.rb @@ -143,7 +143,7 @@ class PostAction < ActiveRecord::Base result = unscoped.where(post_action_type_id: post_action_type) result = result.where('post_actions.created_at >= ?', opts[:start_date] || (opts[:since_days_ago] || 30).days.ago) result = result.where('post_actions.created_at <= ?', opts[:end_date]) if opts[:end_date] - result = result.joins(post: :topic).where('topics.category_id = ?', opts[:category_id]) if opts[:category_id] + result = result.joins(post: :topic).merge(Topic.in_category_and_categories(opts[:category_id])) if opts[:category_id] result.group('date(post_actions.created_at)') .order('date(post_actions.created_at)') .count diff --git a/app/models/report.rb b/app/models/report.rb index 7593f790b37..d7e2b986da1 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -360,7 +360,7 @@ class Report report.category_filtering = true 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 + countable = countable.in_category_and_subcategories(report.category_id) if report.category_id add_counts report, countable, 'topics.created_at' end @@ -369,7 +369,9 @@ class Report report.category_filtering = true basic_report_about report, Post, :public_posts_count_per_day, report.start_date, report.end_date, report.category_id countable = Post.public_posts.where(post_type: Post.types[:regular]) - countable = countable.joins(:topic).where("topics.category_id = ?", report.category_id) if report.category_id + if report.category_id + countable = countable.joins(:topic).merge(Topic.in_category_and_subcategories(report.category_id)) + end add_counts report, countable, 'posts.created_at' end @@ -475,7 +477,7 @@ class Report 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_without_custom.values) - countable = countable.joins(post: :topic).where("topics.category_id = ?", report.category_id) if report.category_id + countable = countable.joins(post: :topic).merge(Topic.in_category_and_subcategories(report.category_id)) if report.category_id add_counts report, countable, 'post_actions.created_at' end @@ -497,7 +499,7 @@ class Report report.data << { x: date, y: count } end 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 + countable = countable.joins(post: :topic).merge(Topic.in_category_and_subcategories(report.category_id)) if report.category_id add_counts report, countable, 'post_actions.created_at' end @@ -600,6 +602,7 @@ class Report end def self.report_top_referred_topics(report) + report.category_filtering = true report.modes = [:table] report.labels = [ @@ -618,13 +621,19 @@ class Report } ] - options = { end_date: report.end_date, start_date: report.start_date, limit: report.limit || 8 } + options = { + end_date: report.end_date, + start_date: report.start_date, + limit: report.limit || 8, + category_id: report.category_id + } result = nil result = IncomingLinksReport.find(:top_referred_topics, options) report.data = result.data end def self.report_top_traffic_sources(report) + report.category_filtering = true report.modes = [:table] report.labels = [ @@ -644,7 +653,12 @@ class Report } ] - options = { end_date: report.end_date, start_date: report.start_date, limit: report.limit || 8 } + options = { + end_date: report.end_date, + start_date: report.start_date, + limit: report.limit || 8, + category_id: report.category_id + } result = nil result = IncomingLinksReport.find(:top_traffic_sources, options) report.data = result.data @@ -1055,6 +1069,7 @@ class Report end def self.report_post_edits(report) + report.category_filtering = true report.modes = [:table] report.labels = [ @@ -1132,7 +1147,16 @@ class Report ON u.id = p.user_id SQL - DB.query(sql).each do |r| + if report.category_id + sql += <<~SQL + JOIN topics t + ON t.id = p.topic_id + WHERE t.category_id = ? OR t.category_id IN (SELECT id FROM categories WHERE categories.parent_category_id = ?) + SQL + end + result = report.category_id ? DB.query(sql, report.category_id, report.category_id) : DB.query(sql) + + result.each do |r| revision = {} revision[:editor_id] = r.editor_id revision[:editor_username] = r.editor_username diff --git a/app/models/topic.rb b/app/models/topic.rb index 94f2a8566fc..f83e1a84a6b 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -190,6 +190,17 @@ class Topic < ActiveRecord::Base where("topics.category_id IS NULL OR topics.category_id IN (SELECT id FROM categories WHERE #{condition[0]})", condition[1]) } + IN_CATEGORY_AND_SUBCATEGORIES_SQL = <<~SQL + t.category_id = :category_id + OR t.category_id IN (SELECT id FROM categories WHERE categories.parent_category_id = :category_id) + SQL + + scope :in_category_and_subcategories, lambda { |category_id| + where("topics.category_id = ? OR topics.category_id IN (SELECT id FROM categories WHERE categories.parent_category_id = ?)", + category_id, + category_id) if category_id + } + scope :with_subtype, ->(subtype) { where('topics.subtype = ?', subtype) } attr_accessor :ignore_category_auto_close @@ -1258,7 +1269,7 @@ class Topic < ActiveRecord::Base builder = DB.build(sql) 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(IN_CATEGORY_AND_SUBCATEGORIES_SQL, 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") @@ -1297,7 +1308,7 @@ class Topic < ActiveRecord::Base builder = DB.build(WITH_NO_RESPONSE_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.category_id = :category_id", category_id: category_id) if category_id + builder.where(IN_CATEGORY_AND_SUBCATEGORIES_SQL, category_id: category_id) if category_id builder.where("t.archetype <> '#{Archetype.private_message}'") builder.where("t.deleted_at IS NULL") builder.query_hash @@ -1317,7 +1328,7 @@ class Topic < ActiveRecord::Base def self.with_no_response_total(opts = {}) builder = DB.build(WITH_NO_RESPONSE_TOTAL_SQL) - builder.where("t.category_id = :category_id", category_id: opts[:category_id]) if opts[:category_id] + builder.where(IN_CATEGORY_AND_SUBCATEGORIES_SQL, category_id: opts[:category_id]) if opts[:category_id] builder.where("t.archetype <> '#{Archetype.private_message}'") builder.where("t.deleted_at IS NULL") builder.query_single.first.to_i diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb index 38af9ce4514..71ab9b549bb 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -15,6 +15,20 @@ describe Report do end end + shared_examples 'category filtering on subcategories' do + before(:all) do + c3 = Fabricate(:category, id: 3) + c2 = Fabricate(:category, id: 2, parent_category_id: 3) + Topic.find(c2.topic_id).delete + Topic.find(c3.topic_id).delete + end + after(:all) do + Category.where(id: 2).or(Category.where(id: 3)).destroy_all + User.where("id > 0").destroy_all + end + include_examples 'category filtering' + end + shared_examples 'with data x/y' do it "returns today's data" do expect(report.data.select { |v| v[:x].today? }).to be_present @@ -717,6 +731,12 @@ describe Report do let(:report) { Report.find('flags', category_id: 2) } include_examples 'category filtering' + + context "on subcategories" do + let(:report) { Report.find('flags', category_id: 3) } + + include_examples 'category filtering on subcategories' + end end end end @@ -740,6 +760,12 @@ describe Report do let(:report) { Report.find('topics', category_id: 2) } include_examples 'category filtering' + + context "on subcategories" do + let(:report) { Report.find('topics', category_id: 3) } + + include_examples 'category filtering on subcategories' + end end end end @@ -777,4 +803,66 @@ describe Report do expect(report.error).to eq(:timeout) end end + + describe 'posts' do + let(:report) { Report.find('posts') } + + include_examples 'no data' + + context 'with data' do + include_examples 'with data x/y' + + before(:each) do + topic = Fabricate(:topic) + topic_with_category_id = Fabricate(:topic, category_id: 2) + Fabricate(:post, topic: topic) + Fabricate(:post, topic: topic_with_category_id) + Fabricate(:post, topic: topic) + Fabricate(:post, created_at: 45.days.ago, topic: topic) + end + + context "with category filtering" do + let(:report) { Report.find('posts', category_id: 2) } + + include_examples 'category filtering' + + context "on subcategories" do + let(:report) { Report.find('posts', category_id: 3) } + + include_examples 'category filtering on subcategories' + end + end + end + end + + # TODO: time_to_first_response + + describe 'topics_with_no_response' do + let(:report) { Report.find('topics_with_no_response') } + + include_examples 'no data' + + context 'with data' do + include_examples 'with data x/y' + + before(:each) do + Fabricate(:topic, category_id: 2) + Fabricate(:post, topic: Fabricate(:topic)) + Fabricate(:topic) + Fabricate(:topic, created_at: 45.days.ago) + end + + context "with category filtering" do + let(:report) { Report.find('topics_with_no_response', category_id: 2) } + + include_examples 'category filtering' + + context "on subcategories" do + let(:report) { Report.find('topics_with_no_response', category_id: 3) } + + include_examples 'category filtering on subcategories' + end + end + end + end end diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index 9b8afb856e6..663e67056c6 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -1366,6 +1366,22 @@ describe Topic do expect(Topic.visible).to include c end end + + describe '#in_category_and_subcategories' do + it 'returns topics in a category and its subcategories' do + c1 = Fabricate(:category) + c2 = Fabricate(:category, parent_category_id: c1.id) + c3 = Fabricate(:category) + + t1 = Fabricate(:topic, category_id: c1.id) + t2 = Fabricate(:topic, category_id: c2.id) + t3 = Fabricate(:topic, category_id: c3.id) + + expect(Topic.in_category_and_subcategories(c1.id)).not_to include(t3) + expect(Topic.in_category_and_subcategories(c1.id)).to include(t2) + expect(Topic.in_category_and_subcategories(c1.id)).to include(t1) + end + end end describe '#private_topic_timer' do