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:
Sam 2018-05-11 13:30:21 +10:00
parent 52d6b0f948
commit 8a783412b7
21 changed files with 324 additions and 107 deletions

View File

@ -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 });
},

View File

@ -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);

View File

@ -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")

View File

@ -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}`;

View File

@ -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);

View File

@ -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}}

View File

@ -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>

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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
}
}
};

View File

@ -60,4 +60,4 @@ QUnit.test("thirtyDayCountTitle", assert => {
assert.ok(title.indexOf('+50%') !== -1);
assert.ok(title.match(/Was 10/));
});
});