UX: improvements to new dashboard
- remove inactive user report and replace with posts - clean up internals so grouping by week happens on client - when switching periods old report was not destroyed leading to bugs - calculate trend based on previous interval ... not previous 30 days - show percentages for mau/dau - be more careful about utc date usage - show uniqu and click through rate on search panel - publish key of report with report so we only load the correct one - subscribe earlier in channel in case of concurrency issues
This commit is contained in:
parent
52d6b0f948
commit
8a783412b7
|
@ -4,21 +4,36 @@ import AsyncReport from "admin/mixins/async-report";
|
|||
import Report from "admin/models/report";
|
||||
import { number } from 'discourse/lib/formatter';
|
||||
|
||||
function collapseWeekly(data, average) {
|
||||
let aggregate = [];
|
||||
let bucket, i;
|
||||
let offset = data.length % 7;
|
||||
for(i = offset; i < data.length; i++) {
|
||||
|
||||
if (bucket && (i % 7 === offset)) {
|
||||
if (average) {
|
||||
bucket.y = parseFloat((bucket.y / 7.0).toFixed(2));
|
||||
}
|
||||
aggregate.push(bucket);
|
||||
bucket = null;
|
||||
}
|
||||
|
||||
bucket = bucket || { x: data[i].x, y: 0 };
|
||||
bucket.y += data[i].y;
|
||||
}
|
||||
return aggregate;
|
||||
}
|
||||
|
||||
export default Ember.Component.extend(AsyncReport, {
|
||||
classNames: ["dashboard-mini-chart"],
|
||||
classNameBindings: ["thirtyDayTrend", "oneDataPoint"],
|
||||
classNameBindings: ["trend", "oneDataPoint"],
|
||||
isLoading: true,
|
||||
thirtyDayTrend: Ember.computed.alias("report.thirtyDayTrend"),
|
||||
trend: Ember.computed.alias("report.trend"),
|
||||
oneDataPoint: false,
|
||||
backgroundColor: "rgba(200,220,240,0.3)",
|
||||
borderColor: "#08C",
|
||||
average: false,
|
||||
|
||||
willDestroyEelement() {
|
||||
this._super();
|
||||
|
||||
this.messageBus.unsubscribe(this.get("dataSource"));
|
||||
},
|
||||
total: 0,
|
||||
|
||||
@computed("dataSourceName")
|
||||
dataSource(dataSourceName) {
|
||||
|
@ -27,9 +42,9 @@ export default Ember.Component.extend(AsyncReport, {
|
|||
}
|
||||
},
|
||||
|
||||
@computed("thirtyDayTrend")
|
||||
trendIcon(thirtyDayTrend) {
|
||||
switch (thirtyDayTrend) {
|
||||
@computed("trend")
|
||||
trendIcon(trend) {
|
||||
switch (trend) {
|
||||
case "trending-up":
|
||||
return "angle-up";
|
||||
case "trending-down":
|
||||
|
@ -47,7 +62,7 @@ export default Ember.Component.extend(AsyncReport, {
|
|||
this.set("isLoading", true);
|
||||
|
||||
let payload = {
|
||||
data: { async: true }
|
||||
data: { async: true, facets: ["prev_period"] }
|
||||
};
|
||||
|
||||
if (this.get("startDate")) {
|
||||
|
@ -58,11 +73,18 @@ export default Ember.Component.extend(AsyncReport, {
|
|||
payload.data.end_date = this.get("endDate").format('YYYY-MM-DD[T]HH:mm:ss.SSSZZ');
|
||||
}
|
||||
|
||||
if (this._chart) {
|
||||
this._chart.destroy();
|
||||
this._chart = null;
|
||||
}
|
||||
|
||||
this.set("report", null);
|
||||
|
||||
ajax(this.get("dataSource"), payload)
|
||||
.then((response) => {
|
||||
// if (!Ember.isEmpty(response.report.data)) {
|
||||
this._setPropertiesFromReport(Report.create(response.report));
|
||||
// }
|
||||
this.set('reportKey', response.report.report_key);
|
||||
|
||||
this.loadReport(response.report);
|
||||
})
|
||||
.finally(() => {
|
||||
if (this.get("oneDataPoint")) {
|
||||
|
@ -77,6 +99,19 @@ export default Ember.Component.extend(AsyncReport, {
|
|||
});
|
||||
},
|
||||
|
||||
loadReport(report) {
|
||||
if (report.data) {
|
||||
Report.fillMissingDates(report);
|
||||
|
||||
if (report.data && report.data.length > 40) {
|
||||
report.data = collapseWeekly(report.data, this.get("average"));
|
||||
}
|
||||
|
||||
const model = Report.create(report);
|
||||
this._setPropertiesFromReport(model);
|
||||
}
|
||||
},
|
||||
|
||||
renderReport() {
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) { return; }
|
||||
if (this.get("oneDataPoint")) return;
|
||||
|
@ -96,6 +131,9 @@ export default Ember.Component.extend(AsyncReport, {
|
|||
}]
|
||||
};
|
||||
|
||||
if (this._chart) {
|
||||
this._chart.destroy();
|
||||
}
|
||||
this._chart = new window.Chart(context, this._buildChartConfig(data));
|
||||
});
|
||||
},
|
||||
|
@ -105,7 +143,6 @@ export default Ember.Component.extend(AsyncReport, {
|
|||
this.get("startDate").isSame(this.get("endDate"), "day");
|
||||
|
||||
report.set("average", this.get("average"));
|
||||
|
||||
this.setProperties({ oneDataPoint, report });
|
||||
},
|
||||
|
||||
|
|
|
@ -13,9 +13,8 @@ export default Ember.Component.extend(AsyncReport, {
|
|||
values(report) {
|
||||
if (!report) return;
|
||||
return Ember.makeArray(report.data)
|
||||
.sort((a, b) => a.x >= b.x)
|
||||
.map(x => {
|
||||
return [ x[0], number(x[1]), number(x[2]) ];
|
||||
return [ x[0], number(x[1]), x[2] ];
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -25,6 +24,10 @@ export default Ember.Component.extend(AsyncReport, {
|
|||
return Ember.makeArray(report.labels);
|
||||
},
|
||||
|
||||
loadReport(report_json) {
|
||||
this._setPropertiesFromReport(Report.create(report_json));
|
||||
},
|
||||
|
||||
fetchReport() {
|
||||
this.set("isLoading", true);
|
||||
|
||||
|
@ -40,7 +43,8 @@ export default Ember.Component.extend(AsyncReport, {
|
|||
|
||||
ajax(this.get("dataSource"), payload)
|
||||
.then((response) => {
|
||||
this._setPropertiesFromReport(Report.create(response.report));
|
||||
this.set('reportKey', response.report.report_key);
|
||||
this.loadReport(response.report);
|
||||
}).finally(() => {
|
||||
if (!Ember.isEmpty(this.get("report.data"))) {
|
||||
this.set("isLoading", false);
|
||||
|
|
|
@ -37,30 +37,34 @@ export default Ember.Controller.extend({
|
|||
|
||||
@computed("period")
|
||||
startDate(period) {
|
||||
let fullDay = moment().utc().subtract(1, "day");
|
||||
|
||||
switch (period) {
|
||||
case "yearly":
|
||||
return moment().subtract(1, "year").startOf("day");
|
||||
return fullDay.subtract(1, "year").startOf("day");
|
||||
break;
|
||||
case "quarterly":
|
||||
return moment().subtract(3, "month").startOf("day");
|
||||
return fullDay.subtract(3, "month").startOf("day");
|
||||
break;
|
||||
case "weekly":
|
||||
return moment().subtract(1, "week").startOf("day");
|
||||
return fullDay.subtract(1, "week").startOf("day");
|
||||
break;
|
||||
case "monthly":
|
||||
return moment().subtract(1, "month").startOf("day");
|
||||
break;
|
||||
case "daily":
|
||||
return moment().startOf("day");
|
||||
return fullDay.subtract(1, "month").startOf("day");
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
@computed("period")
|
||||
endDate(period) {
|
||||
return period === "all" ? null : moment().endOf("day");
|
||||
@computed()
|
||||
lastWeek() {
|
||||
return moment().utc().endOf("day").subtract(1, "week");
|
||||
},
|
||||
|
||||
@computed()
|
||||
endDate() {
|
||||
return moment().utc().subtract(1, "day").endOf("day");
|
||||
},
|
||||
|
||||
@computed("updated_at")
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import Report from "admin/models/report";
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
classNameBindings: ["isLoading"],
|
||||
|
@ -9,23 +8,27 @@ export default Ember.Mixin.create({
|
|||
init() {
|
||||
this._super();
|
||||
|
||||
this.messageBus.subscribe(this.get("dataSource"), report => {
|
||||
const formatDate = (date) => moment(date).format("YYYYMMDD");
|
||||
|
||||
// this check is done to avoid loading a chart after period has changed
|
||||
if (
|
||||
(this.get("startDate") && formatDate(report.start_date) === formatDate(this.get("startDate"))) &&
|
||||
(this.get("endDate") && formatDate(report.end_date) === formatDate(this.get("endDate")))
|
||||
) {
|
||||
this._setPropertiesFromReport(Report.create(report));
|
||||
this.set("isLoading", false);
|
||||
this.renderReport();
|
||||
} else {
|
||||
this._setPropertiesFromReport(Report.create(report));
|
||||
this.set("isLoading", false);
|
||||
this.renderReport();
|
||||
this._channel = this.get("dataSource");
|
||||
this._callback = (report) => {
|
||||
if (report.report_key = this.get("reportKey")) {
|
||||
Em.run.next(() => {
|
||||
if (report.report_key = this.get("reportKey")) {
|
||||
this.loadReport(report);
|
||||
console.log(report);
|
||||
this.set("isLoading", false);
|
||||
this.renderReport();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
// in case we did not subscribe in time ensure we always grab the
|
||||
// last thing on the channel
|
||||
this.messageBus.subscribe(this._channel, this._callback, -2);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
this.messageBus.unsubscribe(this._channel, this._callback);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
|
@ -38,12 +41,13 @@ export default Ember.Mixin.create({
|
|||
|
||||
didUpdateAttrs() {
|
||||
this._super();
|
||||
|
||||
this.fetchReport();
|
||||
},
|
||||
|
||||
renderReport() {},
|
||||
|
||||
loadReport() {},
|
||||
|
||||
@computed("dataSourceName")
|
||||
dataSource(dataSourceName) {
|
||||
return `/admin/reports/${dataSourceName}`;
|
||||
|
|
|
@ -90,6 +90,34 @@ const Report = Discourse.Model.extend({
|
|||
}
|
||||
},
|
||||
|
||||
@computed('data')
|
||||
currentTotal(data){
|
||||
return _.reduce(data, (cur, pair) => cur + pair.y, 0);
|
||||
},
|
||||
|
||||
@computed('data', 'currentTotal')
|
||||
currentAverage(data, total) {
|
||||
return parseFloat((total / parseFloat(data.length)).toFixed(1));
|
||||
},
|
||||
|
||||
@computed('prev_period', 'currentTotal', 'currentAverage')
|
||||
trend(prev, currentTotal, currentAverage) {
|
||||
const total = this.get('average') ? currentAverage : currentTotal;
|
||||
const change = ((total - prev) / total) * 100;
|
||||
|
||||
if (change > 50) {
|
||||
return "high-trending-up";
|
||||
} else if (change > 0) {
|
||||
return "trending-up";
|
||||
} else if (change === 0) {
|
||||
return "no-change";
|
||||
} else if (change < -50) {
|
||||
return "high-trending-down";
|
||||
} else if (change < 0) {
|
||||
return "trending-down";
|
||||
}
|
||||
},
|
||||
|
||||
@computed('prev30Days', 'lastThirtyDaysCount')
|
||||
thirtyDayTrend(prev30Days, lastThirtyDaysCount) {
|
||||
const currentPeriod = lastThirtyDaysCount;
|
||||
|
@ -138,6 +166,20 @@ const Report = Discourse.Model.extend({
|
|||
}
|
||||
},
|
||||
|
||||
@computed('prev_period', 'currentTotal', 'currentAverage')
|
||||
trendTitle(prev, currentTotal, currentAverage) {
|
||||
let current = this.get('average') ? currentAverage : currentTotal;
|
||||
let percent = this.percentChangeString(current, prev);
|
||||
|
||||
if (this.get('average')) {
|
||||
prev = prev.toFixed(1);
|
||||
current += '%';
|
||||
prev += '%';
|
||||
}
|
||||
|
||||
return I18n.t('admin.dashboard.reports.trend_title', {percent: percent, prev: prev, current: current});
|
||||
},
|
||||
|
||||
changeTitle(val1, val2, prevPeriodString) {
|
||||
const percentChange = this.percentChangeString(val1, val2);
|
||||
var title = "";
|
||||
|
@ -176,6 +218,14 @@ const Report = Discourse.Model.extend({
|
|||
|
||||
Report.reopenClass({
|
||||
|
||||
fillMissingDates(report) {
|
||||
if (report.data.length > 0) {
|
||||
const startDateFormatted = moment.utc(report.start_date).format('YYYY-MM-DD');
|
||||
const endDateFormatted = moment.utc(report.end_date).format('YYYY-MM-DD');
|
||||
report.data = fillMissingDates(report.data, startDateFormatted, endDateFormatted);
|
||||
}
|
||||
},
|
||||
|
||||
find(type, startDate, endDate, categoryId, groupId) {
|
||||
return ajax("/admin/reports/" + type, {
|
||||
data: {
|
||||
|
@ -186,11 +236,7 @@ Report.reopenClass({
|
|||
}
|
||||
}).then(json => {
|
||||
// Add zero values for missing dates
|
||||
if (json.report.data.length > 0) {
|
||||
const startDateFormatted = moment(json.report.start_date).utc().format('YYYY-MM-DD');
|
||||
const endDateFormatted = moment(json.report.end_date).utc().format('YYYY-MM-DD');
|
||||
json.report.data = fillMissingDates(json.report.data, startDateFormatted, endDateFormatted);
|
||||
}
|
||||
Report.filleMissingDates(json.report);
|
||||
|
||||
const model = Report.create({ type: type });
|
||||
model.setProperties(json.report);
|
||||
|
|
|
@ -9,9 +9,15 @@
|
|||
</h3>
|
||||
|
||||
<div class="chart-trend {{trend}}">
|
||||
<span title="{{report.thirtyDayCountTitle}}">
|
||||
{{number report.lastThirtyDaysCount}}
|
||||
</span>
|
||||
{{#if average}}
|
||||
<span title="{{report.trendTitle}}">
|
||||
{{report.currentAverage}}%
|
||||
</span>
|
||||
{{else}}
|
||||
<span title="{{report.trendTitle}}">
|
||||
{{number report.currentTotal noTitle="true"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
{{#if trendIcon}}
|
||||
{{d-icon trendIcon}}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
endDate=endDate}}
|
||||
|
||||
{{dashboard-mini-chart
|
||||
dataSourceName="new_contributors"
|
||||
dataSourceName="posts"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
|
@ -35,9 +35,10 @@
|
|||
endDate=endDate}}
|
||||
|
||||
{{dashboard-mini-chart
|
||||
dataSourceName="inactive_users"
|
||||
dataSourceName="new_contributors"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -118,7 +119,7 @@
|
|||
<div class="section-column">
|
||||
{{dashboard-table-trending-search
|
||||
dataSourceName="trending_search"
|
||||
startDate=startDate
|
||||
startDate=lastWeek
|
||||
endDate=endDate}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -22,11 +22,17 @@ class Admin::ReportsController < Admin::AdminController
|
|||
group_id = nil
|
||||
end
|
||||
|
||||
facets = nil
|
||||
if Array === params[:facets]
|
||||
facets = params[:facets].map { |s| s.to_s.to_sym }
|
||||
end
|
||||
|
||||
report = Report.find(report_type,
|
||||
start_date: start_date,
|
||||
end_date: end_date,
|
||||
category_id: category_id,
|
||||
group_id: group_id,
|
||||
facets: facets,
|
||||
async: params[:async])
|
||||
|
||||
raise Discourse::NotFound if report.blank?
|
||||
|
|
|
@ -13,12 +13,14 @@ module Jobs
|
|||
report.end_date = args["end_date"].to_date if args["end_date"]
|
||||
report.category_id = args["category_id"] if args["category_id"]
|
||||
report.group_id = args["group_id"] if args["group_id"]
|
||||
report.facets = args["facets"].map(&:to_sym) if args["facets"]
|
||||
|
||||
Report.send("report_#{type}", report)
|
||||
|
||||
Discourse.cache.write(Report.cache_key(report), report.as_json, force: true, expires_in: 30.minutes)
|
||||
json = report.as_json
|
||||
Discourse.cache.write(Report.cache_key(report), json, force: true, expires_in: 30.minutes)
|
||||
|
||||
MessageBus.publish("/admin/reports/#{type}", report.as_json, user_ids: User.staff.pluck(:id))
|
||||
MessageBus.publish("/admin/reports/#{type}", json, user_ids: User.staff.pluck(:id))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,8 @@ require_dependency 'topic_subtype'
|
|||
class Report
|
||||
|
||||
attr_accessor :type, :data, :total, :prev30Days, :start_date,
|
||||
:end_date, :category_id, :group_id, :labels, :async
|
||||
:end_date, :category_id, :group_id, :labels, :async,
|
||||
:prev_period, :facets
|
||||
|
||||
def self.default_days
|
||||
30
|
||||
|
@ -16,7 +17,15 @@ class Report
|
|||
end
|
||||
|
||||
def self.cache_key(report)
|
||||
"reports:#{report.type}:#{report.start_date.to_date.strftime("%Y%m%d")}:#{report.end_date.to_date.strftime("%Y%m%d")}"
|
||||
(+"reports:") <<
|
||||
[
|
||||
report.type,
|
||||
report.category_id,
|
||||
report.start_date.to_date.strftime("%Y%m%d"),
|
||||
report.end_date.to_date.strftime("%Y%m%d"),
|
||||
report.group_id,
|
||||
report.facets
|
||||
].map(&:to_s).join(':')
|
||||
end
|
||||
|
||||
def self.clear_cache
|
||||
|
@ -33,14 +42,18 @@ class Report
|
|||
yaxis: I18n.t("reports.#{type}.yaxis"),
|
||||
description: I18n.t("reports.#{type}.description"),
|
||||
data: data,
|
||||
total: total,
|
||||
start_date: start_date,
|
||||
end_date: end_date,
|
||||
category_id: category_id,
|
||||
group_id: group_id,
|
||||
prev30Days: self.prev30Days,
|
||||
report_key: Report.cache_key(self),
|
||||
labels: labels
|
||||
}.tap do |json|
|
||||
json[:total] = total if total
|
||||
json[:prev_period] = prev_period if prev_period
|
||||
json[:prev30Days] = self.prev30Days if self.prev30Days
|
||||
|
||||
if type == 'page_view_crawler_reqs'
|
||||
json[:related_report] = Report.find('web_crawlers', start_date: start_date, end_date: end_date)&.as_json
|
||||
end
|
||||
|
@ -61,6 +74,7 @@ class Report
|
|||
report.category_id = opts[:category_id] if opts[:category_id]
|
||||
report.group_id = opts[:group_id] if opts[:group_id]
|
||||
report.async = opts[:async] || false
|
||||
report.facets = opts[:facets] || [:total, :prev30Days]
|
||||
report_method = :"report_#{type}"
|
||||
|
||||
if respond_to?(report_method)
|
||||
|
@ -152,10 +166,19 @@ class Report
|
|||
|
||||
data = User.real.count_by_first_post(report.start_date, report.end_date)
|
||||
|
||||
prev30DaysData = User.real.count_by_first_post(report.start_date - 30.days, report.start_date)
|
||||
report.prev30Days = prev30DaysData.sum { |k, v| v }
|
||||
if report.facets.include?(:prev30Days)
|
||||
prev30DaysData = User.real.count_by_first_post(report.start_date - 30.days, report.start_date)
|
||||
report.prev30Days = prev30DaysData.sum { |k, v| v }
|
||||
end
|
||||
|
||||
report.total = User.real.count_by_first_post
|
||||
if report.facets.include?(:total)
|
||||
report.total = User.real.count_by_first_post
|
||||
end
|
||||
|
||||
if report.facets.include?(:prev_period)
|
||||
prev_period_data = User.real.count_by_first_post(report.start_date - (report.end_date - report.start_date), report.start_date)
|
||||
report.prev_period = prev_period_data.sum { |k, v| v }
|
||||
end
|
||||
|
||||
data.each do |key, value|
|
||||
report.data << { x: key, y: value }
|
||||
|
@ -166,11 +189,20 @@ class Report
|
|||
report.data = []
|
||||
|
||||
data = UserAction.count_daily_engaged_users(report.start_date, report.end_date)
|
||||
prev30DaysData = UserAction.count_daily_engaged_users(report.start_date - 30.days, report.start_date)
|
||||
|
||||
report.total = UserAction.count_daily_engaged_users
|
||||
if report.facets.include?(:prev30Days)
|
||||
prev30DaysData = UserAction.count_daily_engaged_users(report.start_date - 30.days, report.start_date)
|
||||
report.prev30Days = prev30DaysData.sum { |k, v| v }
|
||||
end
|
||||
|
||||
report.prev30Days = prev30DaysData.sum { |k, v| v }
|
||||
if report.facets.include?(:total)
|
||||
report.total = UserAction.count_daily_engaged_users
|
||||
end
|
||||
|
||||
if report.facets.include?(:prev_period)
|
||||
prev_data = UserAction.count_daily_engaged_users(report.start_date - (report.end_date - report.start_date), report.start_date)
|
||||
report.prev_period = prev_data.sum { |k, v| v }
|
||||
end
|
||||
|
||||
data.each do |key, value|
|
||||
report.data << { x: key, y: value }
|
||||
|
@ -186,7 +218,15 @@ class Report
|
|||
if data_point["mau"] == 0
|
||||
0
|
||||
else
|
||||
((data_point["dau"].to_f / data_point["mau"].to_f) * 100).ceil
|
||||
((data_point["dau"].to_f / data_point["mau"].to_f) * 100).ceil(2)
|
||||
end
|
||||
}
|
||||
|
||||
dau_avg = Proc.new { |start_date, end_date|
|
||||
data_points = UserVisit.count_by_active_users(start_date, end_date)
|
||||
if !data_points.empty?
|
||||
sum = data_points.sum { |data_point| compute_dau_by_mau.call(data_point) }
|
||||
(sum.to_f / data_points.count.to_f).ceil(2)
|
||||
end
|
||||
}
|
||||
|
||||
|
@ -194,10 +234,12 @@ class Report
|
|||
report.data << { x: data_point["date"], y: compute_dau_by_mau.call(data_point) }
|
||||
end
|
||||
|
||||
prev_data_points = UserVisit.count_by_active_users(report.start_date - 30.days, report.start_date)
|
||||
if !prev_data_points.empty?
|
||||
sum = prev_data_points.sum { |data_point| compute_dau_by_mau.call(data_point) }
|
||||
report.prev30Days = sum / prev_data_points.count
|
||||
if report.facets.include?(:prev_period)
|
||||
report.prev_period = dau_avg.call(report.start_date - (report.end_date - report.start_date), report.start_date)
|
||||
end
|
||||
|
||||
if report.facets.include?(:prev30Days)
|
||||
report.prev30Days = dau_avg.call(report.start_date - 30.days, report.start_date)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -260,8 +302,23 @@ class Report
|
|||
end
|
||||
|
||||
def self.add_counts(report, subject_class, query_column = 'created_at')
|
||||
report.total = subject_class.count
|
||||
report.prev30Days = subject_class.where("#{query_column} >= ? and #{query_column} < ?", report.start_date - 30.days, report.start_date).count
|
||||
if report.facets.include?(:prev_period)
|
||||
report.prev_period = subject_class
|
||||
.where("#{query_column} >= ? and #{query_column} < ?",
|
||||
(report.start_date - (report.end_date - report.start_date)),
|
||||
report.start_date).count
|
||||
end
|
||||
|
||||
if report.facets.include?(:total)
|
||||
report.total = subject_class.count
|
||||
end
|
||||
|
||||
if report.facets.include?(:prev30Days)
|
||||
report.prev30Days = subject_class
|
||||
.where("#{query_column} >= ? and #{query_column} < ?",
|
||||
report.start_date - 30.days,
|
||||
report.start_date).count
|
||||
end
|
||||
end
|
||||
|
||||
def self.report_users_by_trust_level(report)
|
||||
|
@ -359,24 +416,39 @@ class Report
|
|||
def self.report_trending_search(report)
|
||||
report.data = []
|
||||
|
||||
trends = SearchLog.select("term,
|
||||
COUNT(*) AS searches,
|
||||
SUM(CASE
|
||||
select_sql = <<~SQL
|
||||
term,
|
||||
COUNT(*) AS searches,
|
||||
SUM(CASE
|
||||
WHEN search_result_id IS NOT NULL THEN 1
|
||||
ELSE 0
|
||||
END) AS click_through,
|
||||
COUNT(DISTINCT ip_address) AS unique")
|
||||
COUNT(DISTINCT ip_address) AS unique_searches
|
||||
SQL
|
||||
|
||||
trends = SearchLog.select(select_sql)
|
||||
.where('created_at > ? AND created_at <= ?', report.start_date, report.end_date)
|
||||
.group(:term)
|
||||
.order('COUNT(DISTINCT ip_address) DESC, COUNT(*) DESC')
|
||||
.order('unique_searches DESC, click_through ASC, term ASC')
|
||||
.limit(20).to_a
|
||||
|
||||
report.labels = [:term, :searches, :unique].map { |key|
|
||||
report.labels = [:term, :searches, :click_through].map { |key|
|
||||
I18n.t("reports.trending_search.labels.#{key}")
|
||||
}
|
||||
|
||||
trends.each do |trend|
|
||||
report.data << [trend.term, trend.searches, trend.unique]
|
||||
ctr =
|
||||
if trend.click_through == 0
|
||||
0
|
||||
else
|
||||
trend.click_through.to_f / trend.searches.to_f
|
||||
end
|
||||
|
||||
report.data << [
|
||||
trend.term,
|
||||
trend.unique_searches,
|
||||
(ctr * 100).ceil(1).to_s + "%"
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -459,7 +459,8 @@ class Topic < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.listable_count_per_day(start_date, end_date, category_id = nil)
|
||||
result = listable_topics.smart_group_by_date("topics.created_at", start_date, end_date)
|
||||
result = listable_topics.where("topics.created_at >= ? AND topics.created_at <= ?", start_date, end_date)
|
||||
result = result.group('date(topics.created_at)').order('date(topics.created_at)')
|
||||
result = result.where(category_id: category_id) if category_id
|
||||
result.count
|
||||
end
|
||||
|
|
|
@ -880,7 +880,9 @@ class User < ActiveRecord::Base
|
|||
result = self
|
||||
|
||||
if start_date && end_date
|
||||
result = result.smart_group_by_date("users.created_at", start_date, end_date)
|
||||
result = result.group("date(users.created_at)")
|
||||
result = result.where("users.created_at >= ? AND users.created_at <= ?", start_date, end_date)
|
||||
result = result.order('date(users.created_at)')
|
||||
end
|
||||
|
||||
if group_id
|
||||
|
@ -895,7 +897,9 @@ class User < ActiveRecord::Base
|
|||
result = joins('INNER JOIN user_stats AS us ON us.user_id = users.id')
|
||||
|
||||
if start_date && end_date
|
||||
result = result.smart_group_by_date("us.first_post_created_at", start_date, end_date)
|
||||
result = result.group("date(us.first_post_created_at)")
|
||||
result = result.where("us.first_post_created_at > ? AND us.first_post_created_at < ?", start_date, end_date)
|
||||
result = result.order("date(us.first_post_created_at)")
|
||||
end
|
||||
|
||||
result.count
|
||||
|
|
|
@ -127,7 +127,9 @@ SQL
|
|||
.where(action_type: [LIKE, NEW_TOPIC, REPLY, NEW_PRIVATE_MESSAGE])
|
||||
|
||||
if start_date && end_date
|
||||
result = result.smart_group_by_date(:created_at, start_date, end_date)
|
||||
result = result.group('date(created_at)')
|
||||
result = result.where('created_at > ? AND created_at < ?', start_date, end_date)
|
||||
result = result.order('date(created_at)')
|
||||
end
|
||||
|
||||
result.count
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
class UserVisit < ActiveRecord::Base
|
||||
include DateGroupable
|
||||
|
||||
def self.counts_by_day_query(start_date, end_date, group_id = nil)
|
||||
result = where('visited_at >= ? and visited_at <= ?', start_date.to_date, end_date.to_date)
|
||||
|
||||
|
@ -13,16 +11,14 @@ class UserVisit < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.count_by_active_users(start_date, end_date)
|
||||
aggregation_unit = aggregation_unit_for_period(start_date, end_date)
|
||||
|
||||
sql = <<SQL
|
||||
WITH dau AS (
|
||||
SELECT date_trunc('#{aggregation_unit}', user_visits.visited_at)::DATE AS date,
|
||||
SELECT date_trunc('day', user_visits.visited_at)::DATE AS date,
|
||||
count(distinct user_visits.user_id) AS dau
|
||||
FROM user_visits
|
||||
WHERE user_visits.visited_at::DATE BETWEEN '#{start_date}' AND '#{end_date}'
|
||||
GROUP BY date_trunc('#{aggregation_unit}', user_visits.visited_at)::DATE
|
||||
ORDER BY date_trunc('#{aggregation_unit}', user_visits.visited_at)::DATE
|
||||
WHERE user_visits.visited_at::DATE >= :start_date::DATE AND user_visits.visited_at < :end_date::DATE
|
||||
GROUP BY date_trunc('day', user_visits.visited_at)::DATE
|
||||
ORDER BY date_trunc('day', user_visits.visited_at)::DATE
|
||||
)
|
||||
|
||||
SELECT date, dau,
|
||||
|
@ -33,7 +29,7 @@ class UserVisit < ActiveRecord::Base
|
|||
FROM dau
|
||||
SQL
|
||||
|
||||
UserVisit.exec_sql(sql).to_a
|
||||
UserVisit.exec_sql(sql, start_date: start_date, end_date: end_date).to_a
|
||||
end
|
||||
|
||||
# A count of visits in a date range by day
|
||||
|
|
|
@ -2748,6 +2748,7 @@ en:
|
|||
activity_metrics: Activity Metrics
|
||||
|
||||
reports:
|
||||
trend_title: "%{percent} change. Currently %{current}, was %{prev} in previous period."
|
||||
today: "Today"
|
||||
yesterday: "Yesterday"
|
||||
last_7_days: "Last 7 Days"
|
||||
|
|
|
@ -880,6 +880,7 @@ en:
|
|||
title: "Posts"
|
||||
xaxis: "Day"
|
||||
yaxis: "Number of new posts"
|
||||
description: "New posts created during this period"
|
||||
likes:
|
||||
title: "Likes"
|
||||
xaxis: "Day"
|
||||
|
@ -914,7 +915,7 @@ en:
|
|||
labels:
|
||||
term: Term
|
||||
searches: Searches
|
||||
unique: Unique
|
||||
click_through: Click Through Rate
|
||||
emails:
|
||||
title: "Emails Sent"
|
||||
xaxis: "Day"
|
||||
|
|
|
@ -30,6 +30,7 @@ describe Report do
|
|||
|
||||
describe "topics" do
|
||||
before do
|
||||
Report.clear_cache
|
||||
freeze_time DateTime.parse('2017-03-01 12:00')
|
||||
|
||||
((0..32).to_a + [60, 61, 62, 63]).each do |i|
|
||||
|
@ -37,11 +38,21 @@ describe Report do
|
|||
end
|
||||
end
|
||||
|
||||
subject(:json) { Report.find("topics").as_json }
|
||||
|
||||
it "counts the correct records" do
|
||||
json = Report.find("topics").as_json
|
||||
expect(json[:data].size).to eq(31)
|
||||
expect(json[:prev30Days]).to eq(3)
|
||||
|
||||
# lets make sure we can ask for the correct options for the report
|
||||
json = Report.find("topics",
|
||||
start_date: 5.days.ago.beginning_of_day,
|
||||
end_date: 1.day.ago.end_of_day,
|
||||
facets: [:prev_period]
|
||||
).as_json
|
||||
|
||||
expect(json[:prev_period]).to eq(5)
|
||||
expect(json[:data].length).to eq(5)
|
||||
expect(json[:prev30Days]).to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -321,7 +332,9 @@ describe Report do
|
|||
context "with different searches" do
|
||||
before do
|
||||
SearchLog.log(term: 'ruby', search_type: :header, ip_address: '127.0.0.1')
|
||||
SearchLog.log(term: 'ruby', search_type: :header, ip_address: '127.0.0.1', user_id: Fabricate(:user).id)
|
||||
|
||||
SearchLog.create!(term: 'ruby', search_result_id: 1, search_type: 1, ip_address: '127.0.0.1', user_id: Fabricate(:user).id)
|
||||
|
||||
SearchLog.log(term: 'ruby', search_type: :header, ip_address: '127.0.0.2')
|
||||
SearchLog.log(term: 'php', search_type: :header, ip_address: '127.0.0.1')
|
||||
end
|
||||
|
@ -332,12 +345,11 @@ describe Report do
|
|||
|
||||
it "returns a report with data" do
|
||||
expect(report.data[0][0]).to eq("ruby")
|
||||
expect(report.data[0][1]).to eq(3)
|
||||
expect(report.data[0][2]).to eq(2)
|
||||
expect(report.data[0][1]).to eq(2)
|
||||
expect(report.data[0][2]).to eq('33.4%')
|
||||
|
||||
expect(report.data[1][0]).to eq("php")
|
||||
expect(report.data[1][1]).to eq(1)
|
||||
expect(report.data[1][2]).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -373,7 +385,7 @@ describe Report do
|
|||
|
||||
it "returns a report with data" do
|
||||
expect(report.data.first[:y]).to eq(100)
|
||||
expect(report.data.last[:y]).to eq(34)
|
||||
expect(report.data.last[:y]).to eq(33.34)
|
||||
expect(report.prev30Days).to eq(75)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1746,7 +1746,7 @@ describe Topic do
|
|||
|
||||
describe '#listable_count_per_day' do
|
||||
before(:each) do
|
||||
freeze_time
|
||||
freeze_time DateTime.parse('2017-03-01 12:00')
|
||||
|
||||
Fabricate(:topic)
|
||||
Fabricate(:topic, created_at: 1.day.ago)
|
||||
|
|
|
@ -94,7 +94,7 @@ describe User do
|
|||
describe '#count_by_signup_date' do
|
||||
before(:each) do
|
||||
User.destroy_all
|
||||
freeze_time
|
||||
freeze_time DateTime.parse('2017-02-01 12:00')
|
||||
Fabricate(:user)
|
||||
Fabricate(:user, created_at: 1.day.ago)
|
||||
Fabricate(:user, created_at: 1.day.ago)
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
export default {
|
||||
"/admin/reports/posts": {
|
||||
"report": {
|
||||
"type": "topics",
|
||||
"title": "Topics",
|
||||
"xaxis": "Day",
|
||||
"yaxis": "Number of new posts",
|
||||
"data": null,
|
||||
"total": null,
|
||||
"start_date": "2018-03-26T00:00:00.000Z",
|
||||
"end_date": "2018-04-25T23:59:59.999Z",
|
||||
"category_id": null,
|
||||
"group_id": null,
|
||||
"prev30Days": 0,
|
||||
"labels": null
|
||||
}
|
||||
}
|
||||
};
|
|
@ -60,4 +60,4 @@ QUnit.test("thirtyDayCountTitle", assert => {
|
|||
|
||||
assert.ok(title.indexOf('+50%') !== -1);
|
||||
assert.ok(title.match(/Was 10/));
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue