diff --git a/app/assets/javascripts/admin/models/report.js b/app/assets/javascripts/admin/models/report.js.es6 similarity index 93% rename from app/assets/javascripts/admin/models/report.js rename to app/assets/javascripts/admin/models/report.js.es6 index 6f8bfdfff..d03e6d605 100644 --- a/app/assets/javascripts/admin/models/report.js +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -1,9 +1,11 @@ -Discourse.Report = Discourse.Model.extend({ +import round from "discourse/lib/round"; + +const Report = Discourse.Model.extend({ reportUrl: function() { return("/admin/reports/" + this.get('type')); }.property('type'), - valueAt: function(numDaysAgo) { + valueAt(numDaysAgo) { if (this.data) { var wantedDate = moment().subtract(numDaysAgo, 'days').format('YYYY-MM-DD'); var item = this.data.find( function(d) { return d.x === wantedDate; } ); @@ -14,7 +16,7 @@ Discourse.Report = Discourse.Model.extend({ return 0; }, - sumDays: function(startDaysAgo, endDaysAgo) { + sumDays(startDaysAgo, endDaysAgo) { if (this.data) { var earliestDate = moment().subtract(endDaysAgo, 'days').startOf('day'); var latestDate = moment().subtract(startDaysAgo, 'days').startOf('day'); @@ -25,7 +27,7 @@ Discourse.Report = Discourse.Model.extend({ sum += datum.y; } }); - return sum; + return round(sum, -2); } }, @@ -100,7 +102,7 @@ Discourse.Report = Discourse.Model.extend({ } }.property('type'), - percentChangeString: function(val1, val2) { + percentChangeString(val1, val2) { var val = ((val1 - val2) / val2) * 100; if( isNaN(val) || !isFinite(val) ) { return null; @@ -111,7 +113,7 @@ Discourse.Report = Discourse.Model.extend({ } }, - changeTitle: function(val1, val2, prevPeriodString) { + changeTitle(val1, val2, prevPeriodString) { var title = ''; var percentChange = this.percentChangeString(val1, val2); if( percentChange ) { @@ -139,7 +141,7 @@ Discourse.Report = Discourse.Model.extend({ }); -Discourse.Report.reopenClass({ +Report.reopenClass({ find: function(type, startDate, endDate) { return Discourse.ajax("/admin/reports/" + type, {data: { @@ -162,3 +164,5 @@ Discourse.Report.reopenClass({ }); } }); + +export default Report; diff --git a/app/assets/javascripts/admin/templates/dashboard.hbs b/app/assets/javascripts/admin/templates/dashboard.hbs index 1d4dcf674..5a61c52dd 100644 --- a/app/assets/javascripts/admin/templates/dashboard.hbs +++ b/app/assets/javascripts/admin/templates/dashboard.hbs @@ -58,6 +58,8 @@ {{admin-report-counts report=signups}} {{admin-report-counts report=topics}} {{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=flags}} {{admin-report-counts report=bookmarks}} diff --git a/plugins/poll/assets/javascripts/lib/decimal-adjust.js.es6 b/app/assets/javascripts/discourse/lib/decimal-adjust.js.es6 similarity index 100% rename from plugins/poll/assets/javascripts/lib/decimal-adjust.js.es6 rename to app/assets/javascripts/discourse/lib/decimal-adjust.js.es6 diff --git a/app/assets/javascripts/discourse/lib/formatter.js b/app/assets/javascripts/discourse/lib/formatter.js index f2063e378..2b80eb087 100644 --- a/app/assets/javascripts/discourse/lib/formatter.js +++ b/app/assets/javascripts/discourse/lib/formatter.js @@ -188,7 +188,7 @@ relativeAgeMediumSpan = function(distance, leaveAgo) { }; switch(true){ - case(distanceInMinutes >= 1 && distanceInMinutes <= 56): + case(distanceInMinutes >= 1 && distanceInMinutes <= 55): formatted = t("x_minutes", {count: distanceInMinutes}); break; case(distanceInMinutes >= 56 && distanceInMinutes <= 89): diff --git a/plugins/poll/assets/javascripts/lib/round.js.es6 b/app/assets/javascripts/discourse/lib/round.js.es6 similarity index 54% rename from plugins/poll/assets/javascripts/lib/round.js.es6 rename to app/assets/javascripts/discourse/lib/round.js.es6 index b26eed073..45d4f0eec 100644 --- a/plugins/poll/assets/javascripts/lib/round.js.es6 +++ b/app/assets/javascripts/discourse/lib/round.js.es6 @@ -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) { return decimalAdjust("round", value, exp); diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index 04c199b40..f3d718b2b 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -7,6 +7,8 @@ class AdminDashboardData 'signups', 'topics', 'posts', + 'time_to_first_response', + 'topics_with_no_response', 'flags', 'users_by_trust_level', 'likes', diff --git a/app/models/report.rb b/app/models/report.rb index 76f5952d5..5c6c9ad9d 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -93,6 +93,21 @@ class Report add_counts report, Post.public_posts, 'posts.created_at' 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) report_about report, EmailLog end diff --git a/app/models/topic.rb b/app/models/topic.rb index a5be02dbe..cfdae5d20 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -848,6 +848,66 @@ class Topic < ActiveRecord::Base SiteSetting.embeddable_hosts.present? && SiteSetting.embed_truncate? && has_topic_embed? 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 def update_category_topic_count_by(num) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 89309529d..1330b1cd6 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -675,6 +675,14 @@ en: title: "Total" xaxis: "Day" 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: rails_env_warning: "Your server is running in %{env} mode." diff --git a/plugins/poll/assets/javascripts/components/poll-results-number.js.es6 b/plugins/poll/assets/javascripts/components/poll-results-number.js.es6 index da48d4abb..42087e293 100644 --- a/plugins/poll/assets/javascripts/components/poll-results-number.js.es6 +++ b/plugins/poll/assets/javascripts/components/poll-results-number.js.es6 @@ -1,4 +1,4 @@ -import round from "discourse/plugins/poll/lib/round"; +import round from "discourse/lib/round"; export default Em.Component.extend({ tagName: "span",