mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 10:59:51 +08:00
Add stats to the admin dashboard
This commit is contained in:
parent
24db302e70
commit
8927432a93
38
app/assets/javascripts/admin/helpers/report_helpers.js
Normal file
38
app/assets/javascripts/admin/helpers/report_helpers.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
Get the y value of a report data point by its index. Negative indexes start from the end.
|
||||
|
||||
@method reportValueY
|
||||
@for Handlebars
|
||||
**/
|
||||
Handlebars.registerHelper('valueAtDaysAgo', function(property, i) {
|
||||
var data = Ember.Handlebars.get(this, property);
|
||||
if( data ) {
|
||||
var wantedDate = Date.create(i + ' days ago').format('{yyyy}-{MM}-{dd}');
|
||||
var item = data.find( function(d, i, arr) { return d.x == wantedDate; } );
|
||||
if( item ) {
|
||||
return item.y;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
Sum the given number of data points from the report, starting at the most recent.
|
||||
|
||||
@method sumLast
|
||||
@for Handlebars
|
||||
**/
|
||||
Handlebars.registerHelper('sumLast', function(property, numDays) {
|
||||
var data = Ember.Handlebars.get(this, property);
|
||||
if( data ) {
|
||||
var earliestDate = Date.create(numDays + ' days ago');
|
||||
var sum = 0;
|
||||
data.each(function(d){
|
||||
if(Date.create(d.x) >= earliestDate) {
|
||||
sum += d.y;
|
||||
}
|
||||
});
|
||||
return sum;
|
||||
}
|
||||
});
|
|
@ -2,7 +2,7 @@ Discourse.Report = Discourse.Model.extend({});
|
|||
|
||||
Discourse.Report.reopenClass({
|
||||
find: function(type) {
|
||||
var model = Discourse.Report.create();
|
||||
var model = Discourse.Report.create({type: type});
|
||||
$.ajax("/admin/reports/" + type, {
|
||||
type: 'GET',
|
||||
success: function(json) {
|
||||
|
|
|
@ -11,6 +11,9 @@ Discourse.AdminDashboardRoute = Discourse.Route.extend({
|
|||
if( !c.get('versionCheckedAt') || Date.create('12 hours ago') > c.get('versionCheckedAt') ) {
|
||||
this.checkVersion(c);
|
||||
}
|
||||
if( !c.get('reportsCheckedAt') || Date.create('1 hour ago') > c.get('reportsCheckedAt') ) {
|
||||
this.fetchReports(c);
|
||||
}
|
||||
},
|
||||
|
||||
renderTemplate: function() {
|
||||
|
@ -19,12 +22,20 @@ Discourse.AdminDashboardRoute = Discourse.Route.extend({
|
|||
|
||||
checkVersion: function(c) {
|
||||
if( Discourse.SiteSettings.version_checks ) {
|
||||
c.set('versionCheckedAt', new Date());
|
||||
Discourse.VersionCheck.find().then(function(vc) {
|
||||
c.set('versionCheck', vc);
|
||||
c.set('versionCheckedAt', new Date());
|
||||
c.set('loading', false);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
fetchReports: function(c) {
|
||||
// TODO: use one request to get all reports, or maybe one request for all dashboard data including version check.
|
||||
c.set('reportsCheckedAt', new Date());
|
||||
['visits', 'signups', 'topics', 'posts'].each(function(reportType){
|
||||
c.set(reportType, Discourse.Report.find(reportType));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -42,3 +42,21 @@
|
|||
|
||||
<div class='clearfix'></div>
|
||||
{{/if}}
|
||||
|
||||
<div class="dashboard-stats">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>{{i18n admin.dashboard.reports.today}}</th>
|
||||
<th>{{i18n admin.dashboard.reports.yesterday}}</th>
|
||||
<th>{{i18n admin.dashboard.reports.last_7_days}}</th>
|
||||
<th>{{i18n admin.dashboard.reports.last_30_days}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{ render 'admin_signups' signups }}
|
||||
{{ render 'admin_visits' visits }}
|
||||
{{ render 'admin_topics' topics }}
|
||||
{{ render 'admin_posts' posts }}
|
||||
</table>
|
||||
</div>
|
|
@ -0,0 +1,9 @@
|
|||
{{#if loaded}}
|
||||
<tr>
|
||||
<td class="title">{{title}}</td>
|
||||
<td class="value">{{valueAtDaysAgo data 0}}</td>
|
||||
<td class="value">{{valueAtDaysAgo data 1}}</td>
|
||||
<td class="value">{{sumLast data 7}}</td>
|
||||
<td class="value">{{sumLast data 30}}</td>
|
||||
</tr>
|
||||
{{/if}}
|
9
app/assets/javascripts/admin/views/reports_views.js
Normal file
9
app/assets/javascripts/admin/views/reports_views.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
These views are needed so we can render the same template multiple times on
|
||||
the admin dashboard.
|
||||
**/
|
||||
var opts = { templateName: 'admin/templates/report', tagName: 'tbody' };
|
||||
Discourse.AdminSignupsView = Discourse.View.extend(opts);
|
||||
Discourse.AdminVisitsView = Discourse.View.extend(opts);
|
||||
Discourse.AdminTopicsView = Discourse.View.extend(opts);
|
||||
Discourse.AdminPostsView = Discourse.View.extend(opts);
|
|
@ -268,5 +268,3 @@ Handlebars.registerHelper('personalizedName', function(property, options) {
|
|||
}
|
||||
return Em.String.i18n('you');
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -301,3 +301,21 @@ table {
|
|||
.flaggers { padding: 0 10px; }
|
||||
.last-flagged { padding: 0 10px; }
|
||||
}
|
||||
|
||||
.dashboard-stats {
|
||||
margin-top: 10px;
|
||||
|
||||
table {
|
||||
width: 450px;
|
||||
|
||||
th {
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td.value {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -450,4 +450,8 @@ class Post < ActiveRecord::Base
|
|||
args[:invalidate_oneboxes] = true if invalidate_oneboxes.present?
|
||||
Jobs.enqueue(:process_post, args)
|
||||
end
|
||||
|
||||
def self.count_per_day(since=30.days.ago)
|
||||
where('created_at > ?', since).group('date(created_at)').order('date(created_at)').count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
class Report
|
||||
|
||||
attr_accessor :type, :data
|
||||
attr_accessor :type, :data, :cache
|
||||
|
||||
def self.cache_expiry
|
||||
3600 # In seconds
|
||||
end
|
||||
|
||||
def initialize(type)
|
||||
@type = type
|
||||
@data = nil
|
||||
@cache = true
|
||||
end
|
||||
|
||||
def as_json
|
||||
|
@ -17,21 +22,74 @@ class Report
|
|||
}
|
||||
end
|
||||
|
||||
def self.find(type)
|
||||
def self.find(type, opts={})
|
||||
report_method = :"report_#{type}"
|
||||
return nil unless respond_to?(report_method)
|
||||
|
||||
# Load the report
|
||||
report = Report.new(type)
|
||||
report.cache = false if opts[:cache] == false
|
||||
send(report_method, report)
|
||||
report
|
||||
end
|
||||
|
||||
def self.report_visits(report)
|
||||
report.data = []
|
||||
UserVisit.by_day.each do |date, count|
|
||||
fetch report do
|
||||
UserVisit.by_day(30.days.ago).each do |date, count|
|
||||
report.data << {x: date, y: count}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.report_signups(report)
|
||||
report.data = []
|
||||
fetch report do
|
||||
User.count_by_signup_date(30.days.ago).each do |date, count|
|
||||
report.data << {x: date, y: count}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.report_topics(report)
|
||||
report.data = []
|
||||
fetch report do
|
||||
Topic.count_per_day(30.days.ago).each do |date, count|
|
||||
report.data << {x: date, y: count}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.report_posts(report)
|
||||
report.data = []
|
||||
fetch report do
|
||||
Post.count_per_day(30.days.ago).each do |date, count|
|
||||
report.data << {x: date, y: count}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def self.fetch(report)
|
||||
unless report.cache and $redis
|
||||
yield
|
||||
return
|
||||
end
|
||||
|
||||
data_set = "#{report.type}:data"
|
||||
if $redis.exists(data_set)
|
||||
$redis.get(data_set).split('|').each do |pair|
|
||||
date, count = pair.split(',')
|
||||
report.data << {x: date, y: count.to_i}
|
||||
end
|
||||
else
|
||||
yield
|
||||
$redis.setex data_set, cache_expiry, report.data.map { |item| "#{item[:x]},#{item[:y]}" }.join('|')
|
||||
end
|
||||
rescue Redis::BaseConnectionError
|
||||
yield
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -183,6 +183,10 @@ class Topic < ActiveRecord::Base
|
|||
where("created_at > ?", time_ago)
|
||||
end
|
||||
|
||||
def self.count_per_day(since=30.days.ago)
|
||||
where('created_at > ?', since).group('date(created_at)').order('date(created_at)').count
|
||||
end
|
||||
|
||||
def private_message?
|
||||
self.archetype == Archetype.private_message
|
||||
end
|
||||
|
|
|
@ -458,6 +458,10 @@ class User < ActiveRecord::Base
|
|||
Summarize.new(bio_cooked).summary
|
||||
end
|
||||
|
||||
def self.count_by_signup_date(since=30.days.ago)
|
||||
where('created_at > ?', since).group('date(created_at)').order('date(created_at)').count
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def cook
|
||||
|
|
|
@ -2,7 +2,7 @@ class UserVisit < ActiveRecord::Base
|
|||
attr_accessible :visited_at, :user_id
|
||||
|
||||
# A list of visits in the last month by day
|
||||
def self.by_day
|
||||
where("visited_at > ?", 1.month.ago).group(:visited_at).order(:visited_at).count
|
||||
def self.by_day(since=30.days.ago)
|
||||
where("visited_at > ?", since).group(:visited_at).order(:visited_at).count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -673,6 +673,13 @@ en:
|
|||
latest_version: "Latest version"
|
||||
update_often: 'Please update often!'
|
||||
|
||||
reports:
|
||||
today: "Today"
|
||||
yesterday: "Yesterday"
|
||||
last_7_days: "Last 7 Days"
|
||||
last_30_days: "Last 30 Days"
|
||||
all_time: "All Time"
|
||||
|
||||
flags:
|
||||
title: "Flags"
|
||||
old: "Old"
|
||||
|
|
|
@ -244,9 +244,13 @@ en:
|
|||
|
||||
reports:
|
||||
visits:
|
||||
title: "Users Visits by Day"
|
||||
xaxis: "Day"
|
||||
yaxis: "Visits"
|
||||
title: "Users Visits"
|
||||
signups:
|
||||
title: "New Users"
|
||||
topics:
|
||||
title: "New Topics"
|
||||
posts:
|
||||
title: "New Posts"
|
||||
|
||||
site_settings:
|
||||
default_locale: "The default language of this Discourse instance (ISO 639-1 Code)"
|
||||
|
|
|
@ -5,7 +5,7 @@ describe Report do
|
|||
|
||||
describe 'visits report' do
|
||||
|
||||
let(:report) { Report.find('visits') }
|
||||
let(:report) { Report.find('visits', cache: false) }
|
||||
|
||||
context "no visits" do
|
||||
it "returns an empty report" do
|
||||
|
@ -30,5 +30,108 @@ describe Report do
|
|||
|
||||
end
|
||||
|
||||
[:signup, :topic, :post].each do |arg|
|
||||
describe "#{arg} report" do
|
||||
pluralized = arg.to_s.pluralize
|
||||
|
||||
let(:report) { Report.find(pluralized, cache: false) }
|
||||
|
||||
context "no #{pluralized}" do
|
||||
it 'returns an empty report' do
|
||||
report.data.should be_blank
|
||||
end
|
||||
end
|
||||
|
||||
context "with #{pluralized}" do
|
||||
before do
|
||||
fabricator = (arg == :signup ? :user : arg)
|
||||
Fabricate(fabricator, created_at: 2.days.ago)
|
||||
Fabricate(fabricator, created_at: 1.day.ago)
|
||||
Fabricate(fabricator, created_at: 1.day.ago)
|
||||
end
|
||||
|
||||
it 'returns correct data' do
|
||||
report.data[0][:y].should == 1
|
||||
report.data[1][:y].should == 2
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#fetch' do
|
||||
context 'signups' do
|
||||
let(:report) { Report.find('signups', cache: true) }
|
||||
|
||||
context 'no data' do
|
||||
context 'cache miss' do
|
||||
before do
|
||||
$redis.expects(:exists).with('signups:data').returns(false)
|
||||
end
|
||||
|
||||
it 'should cache an empty data set' do
|
||||
$redis.expects(:setex).with('signups:data', Report.cache_expiry, "")
|
||||
report.data.should be_blank
|
||||
end
|
||||
end
|
||||
|
||||
context 'cache hit' do
|
||||
before do
|
||||
$redis.expects(:exists).with('signups:data').returns(true)
|
||||
end
|
||||
|
||||
it 'returns the cached empty report' do
|
||||
User.expects(:count_by_signup_date).never
|
||||
$redis.expects(:setex).never
|
||||
$redis.expects(:get).with('signups:data').returns('')
|
||||
report.data.should be_blank
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with data' do
|
||||
before do
|
||||
Fabricate(:user, created_at: 2.days.ago)
|
||||
Fabricate(:user, created_at: 1.day.ago)
|
||||
Fabricate(:user, created_at: 1.day.ago)
|
||||
end
|
||||
|
||||
context 'cache miss' do
|
||||
before do
|
||||
$redis.expects(:exists).with('signups:data').returns(false)
|
||||
end
|
||||
|
||||
it 'should cache the data set' do
|
||||
$redis.expects(:setex).with do |key, expiry, string|
|
||||
key == 'signups:data' and
|
||||
expiry == Report.cache_expiry and
|
||||
string.include? "#{2.days.ago.to_date.to_s},1" and
|
||||
string.include? "#{1.day.ago.to_date.to_s},2"
|
||||
end
|
||||
report()
|
||||
end
|
||||
|
||||
it 'should return correct data' do
|
||||
report.data[0][:y].should == 1
|
||||
report.data[1][:y].should == 2
|
||||
end
|
||||
end
|
||||
|
||||
context 'cache hit' do
|
||||
before do
|
||||
$redis.expects(:exists).with('signups:data').returns(true)
|
||||
end
|
||||
|
||||
it 'returns the cached data' do
|
||||
User.expects(:count_by_signup_date).never
|
||||
$redis.expects(:setex).never
|
||||
$redis.expects(:get).with('signups:data').returns("#{2.days.ago.to_date.to_s},1|#{1.day.ago.to_date.to_s},2")
|
||||
report.data[0][:y].should == 1
|
||||
report.data[1][:y].should == 2
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue
Block a user