UX: Improve category filtering and include subcategories
* category_filtering 1. report_top_referred_topics 2. report_top_traffic_sources 3. report_post_edit * category_filtering with subcategory topics 1. report_top_referred_topics 2. report_top_traffic_sources 3. report_post_edit 4. report_posts 5. report_topics 6. report_topics_with_no_response * category_filtering tests (without subcategory topics) 1. report_posts 2. report_topics_with_no_response * subcategory topics tests `in_category_and_subcategories` in `topic_spec.rb` 1. `in_category_and_subcategories` in `topic_spec.rb` 2. topics, posts, flags and topics_with_no_response in `report_spec.rb`
This commit is contained in:
parent
ef4b9f98c1
commit
6db623ef6b
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue