From 38ed86d0c5231b43ae996e2b7bc6a3dff1c308fe Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 1 May 2013 18:12:02 -0400 Subject: [PATCH] Add reports for IncomingLinks on admin dashboard --- .../admin/routes/admin_dashboard_route.js | 3 + .../admin/templates/dashboard.js.handlebars | 70 ++++++++++++ app/assets/stylesheets/admin/admin_base.scss | 10 +- app/models/admin_dashboard_data.rb | 6 +- app/models/incoming_link.rb | 1 + app/models/incoming_links_report.rb | 107 ++++++++++++++++++ config/locales/server.en.yml | 15 +++ spec/models/incoming_links_report_spec.rb | 98 ++++++++++++++++ 8 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 app/models/incoming_links_report.rb create mode 100644 spec/models/incoming_links_report_spec.rb diff --git a/app/assets/javascripts/admin/routes/admin_dashboard_route.js b/app/assets/javascripts/admin/routes/admin_dashboard_route.js index 72d097c250c..a29ee9af853 100644 --- a/app/assets/javascripts/admin/routes/admin_dashboard_route.js +++ b/app/assets/javascripts/admin/routes/admin_dashboard_route.js @@ -31,6 +31,9 @@ Discourse.AdminDashboardRoute = Discourse.Route.extend({ c.set('admins', d.admins); c.set('moderators', d.moderators); c.set('problems', d.problems); + c.set('top_referrers', d.top_referrers); + c.set('top_traffic_sources', d.top_traffic_sources); + c.set('top_referred_topics', d.top_referred_topics); c.set('loading', false); }); } else if( !c.get('problemsFetchedAt') || Date.create(c.problemsCheckInterval, 'en') > c.get('problemsFetchedAt') ) { diff --git a/app/assets/javascripts/admin/templates/dashboard.js.handlebars b/app/assets/javascripts/admin/templates/dashboard.js.handlebars index d2f600f87f9..736876f0bba 100644 --- a/app/assets/javascripts/admin/templates/dashboard.js.handlebars +++ b/app/assets/javascripts/admin/templates/dashboard.js.handlebars @@ -167,7 +167,77 @@ +
{{ render admin_github_commits githubCommits }} + +
+ + + + + + + + + {{#unless loading}} + {{#each top_referrers.data}} + + + + + + + + {{/each}} + {{/unless}} +
{{top_referrers.title}}{{top_referrers.ytitles.num_visits}}{{top_referrers.ytitles.num_topics}}
{{#linkTo adminUser username}}{{username}}{{/linkTo}}{{num_visits}}{{num_topics}}
+
+ +
+ + + + + + + + {{#unless loading}} + {{#each data in top_referred_topics.data}} + + + + + + + {{/each}} + {{/unless}} +
{{top_referred_topics.title}}{{top_referred_topics.ytitles.num_visits}}
{{shorten data.topic_title}}{{data.num_visits}}
+
+ +
+ + + + + + + + + + {{#unless loading}} + {{#each top_traffic_sources.data}} + + + + + + + + + {{/each}} + {{/unless}} +
{{top_traffic_sources.title}}{{top_traffic_sources.ytitles.num_visits}}{{top_traffic_sources.ytitles.num_topics}}{{top_traffic_sources.ytitles.num_users}}
{{domain}}{{num_visits}}{{num_topics}}{{num_users}}
+
diff --git a/app/assets/stylesheets/admin/admin_base.scss b/app/assets/stylesheets/admin/admin_base.scss index bd7e3e6fb18..234a6fe9d36 100644 --- a/app/assets/stylesheets/admin/admin_base.scss +++ b/app/assets/stylesheets/admin/admin_base.scss @@ -289,6 +289,11 @@ table { @include small-width { width: 390px; } + + .dashboard-stats { + width: 100%; + margin-left: 0; + } } .version-check { @@ -455,7 +460,8 @@ table { .commits-widget { border: solid 1px #ccc; width: 500px; - height: 700px; + height: 300px; + margin-bottom: 36px; @include medium-width { width: 430px; @@ -500,7 +506,7 @@ table { } .commits-list { - height: 669px; + height: 269px; overflow-y:auto; li { diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index b644bcd9ff9..a7821356618 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -48,7 +48,10 @@ class AdminDashboardData reports: REPORTS.map { |type| Report.find(type) }, problems: problems, admins: User.admins.count, - moderators: User.moderators.count + moderators: User.moderators.count, + top_referrers: IncomingLinksReport.find('top_referrers'), + top_traffic_sources: IncomingLinksReport.find('top_traffic_sources'), + top_referred_topics: IncomingLinksReport.find('top_referred_topics') }.merge( SiteSetting.version_checks? ? {version_check: DiscourseUpdates.check_version} : {} ) @@ -117,4 +120,5 @@ class AdminDashboardData def title_check I18n.t('dashboard.title_nag') if SiteSetting.title == SiteSetting.defaults[:title] end + end \ No newline at end of file diff --git a/app/models/incoming_link.rb b/app/models/incoming_link.rb index 5bd850d5c4b..d077edcab04 100644 --- a/app/models/incoming_link.rb +++ b/app/models/incoming_link.rb @@ -1,5 +1,6 @@ class IncomingLink < ActiveRecord::Base belongs_to :topic + belongs_to :user validates :url, presence: true diff --git a/app/models/incoming_links_report.rb b/app/models/incoming_links_report.rb new file mode 100644 index 00000000000..2df865ceb4b --- /dev/null +++ b/app/models/incoming_links_report.rb @@ -0,0 +1,107 @@ +class IncomingLinksReport + + attr_accessor :type, :data, :y_titles + + def initialize(type) + @type = type + @y_titles = {} + @data = nil + end + + def as_json + { + type: self.type, + title: I18n.t("reports.#{self.type}.title"), + xaxis: I18n.t("reports.#{self.type}.xaxis"), + ytitles: self.y_titles, + data: self.data + } + end + + def self.find(type, opts={}) + report_method = :"report_#{type}" + return nil unless respond_to?(report_method) + + # Load the report + report = IncomingLinksReport.new(type) + send(report_method, report) + report + end + + # Return top 10 users who brought traffic to the site within the last 30 days + def self.report_top_referrers(report) + report.y_titles[:num_visits] = I18n.t("reports.#{report.type}.num_visits") + report.y_titles[:num_topics] = I18n.t("reports.#{report.type}.num_topics") + + num_visits = link_count_per_user + num_topics = topic_count_per_user + report.data = [] + num_visits.keys.each do |username| + report.data << {username: username, num_visits: num_visits[username], num_topics: num_topics[username]} + end + report.data.sort_by! {|x| x[:num_visits]}.reverse![0,10] + end + + def self.per_user + @per_user_query ||= IncomingLink.where('incoming_links.created_at > ? AND incoming_links.user_id IS NOT NULL', 30.days.ago).joins(:user).group('users.username') + end + + def self.link_count_per_user + per_user.count + end + + def self.topic_count_per_user + per_user.count('incoming_links.topic_id', distinct: true) + end + + + # Return top 10 domains that brought traffic to the site within the last 30 days + def self.report_top_traffic_sources(report) + report.y_titles[:num_visits] = I18n.t("reports.#{report.type}.num_visits") + 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_visits = link_count_per_domain + num_topics = topic_count_per_domain + num_users = user_count_per_domain + report.data = [] + num_visits.keys.each do |domain| + report.data << {domain: domain, num_visits: num_visits[domain], num_topics: num_topics[domain], num_users: num_users[domain]} + end + report.data.sort_by! {|x| x[:num_visits]}.reverse![0,10] + end + + def self.per_domain + @per_domain_query ||= IncomingLink.where('created_at > ? AND domain IS NOT NULL', 30.days.ago).group('domain') + end + + def self.link_count_per_domain + per_domain.count + end + + def self.topic_count_per_domain + per_domain.count('topic_id', distinct: true) + end + + def self.user_count_per_domain + per_domain.count('user_id', distinct: true) + end + + + def self.report_top_referred_topics(report) + report.y_titles[:num_visits] = I18n.t("reports.#{report.type}.num_visits") + num_visits = link_count_per_topic + num_visits = num_visits.to_a.sort_by {|x| x[1]}.last(10).reverse # take the top 10 + report.data = [] + topics = Topic.select('id, slug, title').find(num_visits.map {|z| z[0]}) + num_visits.each do |topic_id, num_visits| + topic = topics.find {|t| t.id == topic_id} + report.data << {topic_id: topic_id, topic_title: topic.title, topic_slug: topic.slug, num_visits: num_visits} + end + report.data.sort_by! {|x| x[:num_visits]}.reverse![0,10] + end + + def self.link_count_per_topic + IncomingLink.where('created_at > ? AND topic_id IS NOT NULL', 30.days.ago).group('topic_id').count + end +end \ No newline at end of file diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 4401f96df2d..dfd0029528e 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -334,6 +334,21 @@ en: title: "Nofity User" xaxis: "Day" yaxis: "Number of private messages" + top_referrers: + title: "Top Referrers" + xaxis: "User" + num_visits: "Visits" + num_topics: "Topics" + top_traffic_sources: + title: "Top Traffic Sources" + xaxis: "Domain" + num_visits: "Visits" + num_topics: "Topics" + num_users: "Users" + top_referred_topics: + title: "Top Referred Topics" + xaxis: "Topic" + num_visits: "Visits" dashboard: rails_env_warning: "Your server is running in %{env} mode." diff --git a/spec/models/incoming_links_report_spec.rb b/spec/models/incoming_links_report_spec.rb new file mode 100644 index 00000000000..632858c7062 --- /dev/null +++ b/spec/models/incoming_links_report_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' + +describe IncomingLinksReport do + + describe 'top_referrers' do + subject(:top_referrers) { IncomingLinksReport.find('top_referrers').as_json } + + def stub_empty_referrers_data + IncomingLinksReport.stubs(:link_count_per_user).returns({}) + IncomingLinksReport.stubs(:topic_count_per_user).returns({}) + end + + it 'returns localized titles' do + stub_empty_referrers_data + top_referrers[:title].should be_present + top_referrers[:xaxis].should be_present + top_referrers[:ytitles].should be_present + top_referrers[:ytitles][:num_visits].should be_present + top_referrers[:ytitles][:num_topics].should be_present + end + + it 'with no IncomingLink records, it returns correct data' do + stub_empty_referrers_data + top_referrers[:data].should have(0).records + end + + it 'with some IncomingLink records, it returns correct data' do + IncomingLinksReport.stubs(:link_count_per_user).returns({'luke' => 4, 'chewie' => 2}) + IncomingLinksReport.stubs(:topic_count_per_user).returns({'luke' => 2, 'chewie' => 1}) + top_referrers[:data][0].should == {username: 'luke', num_visits: 4, num_topics: 2} + top_referrers[:data][1].should == {username: 'chewie', num_visits: 2, num_topics: 1} + end + end + + describe 'top_traffic_sources' do + subject(:top_traffic_sources) { IncomingLinksReport.find('top_traffic_sources').as_json } + + def stub_empty_traffic_source_data + IncomingLinksReport.stubs(:link_count_per_domain).returns({}) + IncomingLinksReport.stubs(:topic_count_per_domain).returns({}) + IncomingLinksReport.stubs(:user_count_per_domain).returns({}) + end + + it 'returns localized titles' do + stub_empty_traffic_source_data + top_traffic_sources[:title].should be_present + top_traffic_sources[:xaxis].should be_present + top_traffic_sources[:ytitles].should be_present + top_traffic_sources[:ytitles][:num_visits].should be_present + top_traffic_sources[:ytitles][:num_topics].should be_present + top_traffic_sources[:ytitles][:num_users].should be_present + end + + it 'with no IncomingLink records, it returns correct data' do + stub_empty_traffic_source_data + top_traffic_sources[:data].should have(0).records + end + + it 'with some IncomingLink records, it returns correct data' do + IncomingLinksReport.stubs(:link_count_per_domain).returns({'twitter.com' => 8, 'facebook.com' => 3}) + IncomingLinksReport.stubs(:topic_count_per_domain).returns({'twitter.com' => 2, 'facebook.com' => 3}) + IncomingLinksReport.stubs(:user_count_per_domain).returns({'twitter.com' => 4, 'facebook.com' => 1}) + top_traffic_sources[:data][0].should == {domain: 'twitter.com', num_visits: 8, num_topics: 2, num_users: 4} + top_traffic_sources[:data][1].should == {domain: 'facebook.com', num_visits: 3, num_topics: 3, num_users: 1} + end + end + + describe 'top_referred_topics' do + subject(:top_referred_topics) { IncomingLinksReport.find('top_referred_topics').as_json } + + def stub_empty_referred_topics_data + IncomingLinksReport.stubs(:link_count_per_topic).returns({}) + end + + it 'returns localized titles' do + stub_empty_referred_topics_data + top_referred_topics[:title].should be_present + top_referred_topics[:xaxis].should be_present + top_referred_topics[:ytitles].should be_present + top_referred_topics[:ytitles][:num_visits].should be_present + end + + it 'with no IncomingLink records, it returns correct data' do + stub_empty_referred_topics_data + top_referred_topics[:data].should have(0).records + end + + it 'with some IncomingLink records, it returns correct data' do + topic1 = Fabricate.build(:topic, id: 123); topic2 = Fabricate.build(:topic, id: 234) + IncomingLinksReport.stubs(:link_count_per_topic).returns({topic1.id => 8, topic2.id => 3}) + Topic.stubs(:select).returns(Topic) # bypass the select method + Topic.stubs(:find).returns([topic1, topic2]) + top_referred_topics[:data][0].should == {topic_id: topic1.id, topic_title: topic1.title, topic_slug: topic1.slug, num_visits: 8 } + top_referred_topics[:data][1].should == {topic_id: topic2.id, topic_title: topic2.title, topic_slug: topic2.slug, num_visits: 3 } + end + end + +end