FEATURE: 2 new reports: time to first response, topics with no response

FIX: relativeAgeMediumSpan was off by 1
REFACTOR: extracted decimalAdjust & round functions from the poll plugin
This commit is contained in:
Régis Hanol 2015-06-22 19:46:51 +02:00
parent 0bfabed2d5
commit b25a16ee3e
10 changed files with 101 additions and 10 deletions

View file

@ -1,9 +1,11 @@
Discourse.Report = Discourse.Model.extend({ import round from "discourse/lib/round";
const Report = Discourse.Model.extend({
reportUrl: function() { reportUrl: function() {
return("/admin/reports/" + this.get('type')); return("/admin/reports/" + this.get('type'));
}.property('type'), }.property('type'),
valueAt: function(numDaysAgo) { valueAt(numDaysAgo) {
if (this.data) { if (this.data) {
var wantedDate = moment().subtract(numDaysAgo, 'days').format('YYYY-MM-DD'); var wantedDate = moment().subtract(numDaysAgo, 'days').format('YYYY-MM-DD');
var item = this.data.find( function(d) { return d.x === wantedDate; } ); var item = this.data.find( function(d) { return d.x === wantedDate; } );
@ -14,7 +16,7 @@ Discourse.Report = Discourse.Model.extend({
return 0; return 0;
}, },
sumDays: function(startDaysAgo, endDaysAgo) { sumDays(startDaysAgo, endDaysAgo) {
if (this.data) { if (this.data) {
var earliestDate = moment().subtract(endDaysAgo, 'days').startOf('day'); var earliestDate = moment().subtract(endDaysAgo, 'days').startOf('day');
var latestDate = moment().subtract(startDaysAgo, 'days').startOf('day'); var latestDate = moment().subtract(startDaysAgo, 'days').startOf('day');
@ -25,7 +27,7 @@ Discourse.Report = Discourse.Model.extend({
sum += datum.y; sum += datum.y;
} }
}); });
return sum; return round(sum, -2);
} }
}, },
@ -100,7 +102,7 @@ Discourse.Report = Discourse.Model.extend({
} }
}.property('type'), }.property('type'),
percentChangeString: function(val1, val2) { percentChangeString(val1, val2) {
var val = ((val1 - val2) / val2) * 100; var val = ((val1 - val2) / val2) * 100;
if( isNaN(val) || !isFinite(val) ) { if( isNaN(val) || !isFinite(val) ) {
return null; return null;
@ -111,7 +113,7 @@ Discourse.Report = Discourse.Model.extend({
} }
}, },
changeTitle: function(val1, val2, prevPeriodString) { changeTitle(val1, val2, prevPeriodString) {
var title = ''; var title = '';
var percentChange = this.percentChangeString(val1, val2); var percentChange = this.percentChangeString(val1, val2);
if( percentChange ) { if( percentChange ) {
@ -139,7 +141,7 @@ Discourse.Report = Discourse.Model.extend({
}); });
Discourse.Report.reopenClass({ Report.reopenClass({
find: function(type, startDate, endDate) { find: function(type, startDate, endDate) {
return Discourse.ajax("/admin/reports/" + type, {data: { return Discourse.ajax("/admin/reports/" + type, {data: {
@ -162,3 +164,5 @@ Discourse.Report.reopenClass({
}); });
} }
}); });
export default Report;

View file

@ -58,6 +58,8 @@
{{admin-report-counts report=signups}} {{admin-report-counts report=signups}}
{{admin-report-counts report=topics}} {{admin-report-counts report=topics}}
{{admin-report-counts report=posts}} {{admin-report-counts report=posts}}
{{admin-report-counts report=time_to_first_response}}
{{admin-report-counts report=topics_with_no_response}}
{{admin-report-counts report=likes}} {{admin-report-counts report=likes}}
{{admin-report-counts report=flags}} {{admin-report-counts report=flags}}
{{admin-report-counts report=bookmarks}} {{admin-report-counts report=bookmarks}}

View file

@ -188,7 +188,7 @@ relativeAgeMediumSpan = function(distance, leaveAgo) {
}; };
switch(true){ switch(true){
case(distanceInMinutes >= 1 && distanceInMinutes <= 56): case(distanceInMinutes >= 1 && distanceInMinutes <= 55):
formatted = t("x_minutes", {count: distanceInMinutes}); formatted = t("x_minutes", {count: distanceInMinutes});
break; break;
case(distanceInMinutes >= 56 && distanceInMinutes <= 89): case(distanceInMinutes >= 56 && distanceInMinutes <= 89):

View file

@ -1,4 +1,4 @@
import decimalAdjust from "discourse/plugins/poll/lib/decimal-adjust"; import decimalAdjust from "discourse/lib/decimal-adjust";
export default function(value, exp) { export default function(value, exp) {
return decimalAdjust("round", value, exp); return decimalAdjust("round", value, exp);

View file

@ -7,6 +7,8 @@ class AdminDashboardData
'signups', 'signups',
'topics', 'topics',
'posts', 'posts',
'time_to_first_response',
'topics_with_no_response',
'flags', 'flags',
'users_by_trust_level', 'users_by_trust_level',
'likes', 'likes',

View file

@ -93,6 +93,21 @@ class Report
add_counts report, Post.public_posts, 'posts.created_at' add_counts report, Post.public_posts, 'posts.created_at'
end 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|
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)
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)
end
def self.report_emails(report) def self.report_emails(report)
report_about report, EmailLog report_about report, EmailLog
end end

View file

@ -848,6 +848,66 @@ class Topic < ActiveRecord::Base
SiteSetting.embeddable_hosts.present? && SiteSetting.embed_truncate? && has_topic_embed? SiteSetting.embeddable_hosts.present? && SiteSetting.embed_truncate? && has_topic_embed?
end end
TIME_TO_FIRST_RESPONSE_SQL ||= <<-SQL
SELECT AVG(t.hours)::float AS "hours", t.created_at AS "date"
FROM (
SELECT t.id, t.created_at::date AS created_at, EXTRACT(EPOCH FROM MIN(p.created_at) - t.created_at)::float / 3600.0 AS "hours"
FROM topics t
LEFT JOIN posts p ON p.topic_id = t.id
/*where*/
GROUP BY t.id
) t
GROUP BY t.created_at
ORDER BY t.created_at
SQL
TIME_TO_FIRST_RESPONSE_TOTAL_SQL ||= <<-SQL
SELECT AVG(t.hours)::float AS "hours"
FROM (
SELECT t.id, EXTRACT(EPOCH FROM MIN(p.created_at) - t.created_at)::float / 3600.0 AS "hours"
FROM topics t
LEFT JOIN posts p ON p.topic_id = t.id
/*where*/
GROUP BY t.id
) t
SQL
def self.time_to_first_response(sql, start_date=nil, end_date=nil)
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.archetype <> '#{Archetype.private_message}'")
builder.where("t.deleted_at IS NULL")
builder.where("p.deleted_at IS NULL")
builder.where("p.post_number > 1")
builder.where("EXTRACT(EPOCH FROM p.created_at - t.created_at) > 0")
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)
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)
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
end
def self.with_no_response_total(start_date=nil, end_date=nil)
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.count
end
private private
def update_category_topic_count_by(num) def update_category_topic_count_by(num)

View file

@ -675,6 +675,14 @@ en:
title: "Total" title: "Total"
xaxis: "Day" xaxis: "Day"
yaxis: "Total requests" yaxis: "Total requests"
time_to_first_response:
title: "Time to first response"
xaxis: "Day"
yaxis: "Average time (hours)"
topics_with_no_response:
title: "Topics with no response"
xaxis: "Day"
yaxis: "Total"
dashboard: dashboard:
rails_env_warning: "Your server is running in %{env} mode." rails_env_warning: "Your server is running in %{env} mode."

View file

@ -1,4 +1,4 @@
import round from "discourse/plugins/poll/lib/round"; import round from "discourse/lib/round";
export default Em.Component.extend({ export default Em.Component.extend({
tagName: "span", tagName: "span",