dashboard next: activity metrics and new contributors

This commit also introduces a better grouping of data points.
This commit is contained in:
Joffrey JAFFEUX 2018-04-26 14:49:41 +02:00 committed by GitHub
parent b26e27bdab
commit 9fabf2543b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 260 additions and 128 deletions

View File

@ -2,7 +2,7 @@ import { ajax } from 'discourse/lib/ajax';
import computed from 'ember-addons/ember-computed-decorators'; import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({ export default Ember.Component.extend({
classNames: ["dashboard-table", "dashboard-inline-table"], classNames: ["dashboard-table", "dashboard-inline-table", "fixed"],
classNameBindings: ["isLoading"], classNameBindings: ["isLoading"],

View File

@ -1,6 +1,7 @@
import { ajax } from 'discourse/lib/ajax'; import { ajax } from "discourse/lib/ajax";
import computed from 'ember-addons/ember-computed-decorators'; import computed from "ember-addons/ember-computed-decorators";
import loadScript from 'discourse/lib/load-script'; import loadScript from "discourse/lib/load-script";
import Report from "admin/models/report";
export default Ember.Component.extend({ export default Ember.Component.extend({
classNames: ["dashboard-mini-chart"], classNames: ["dashboard-mini-chart"],
@ -17,12 +18,26 @@ export default Ember.Component.extend({
didInsertElement() { didInsertElement() {
this._super(); this._super();
this._initializeChart();
if (this.get("model")) {
loadScript("/javascripts/Chart.min.js").then(() => {
this._setPropertiesFromModel(this.get("model"));
this._drawChart();
});
}
}, },
didUpdateAttrs() { didUpdateAttrs() {
this._super(); this._super();
this._initializeChart();
loadScript("/javascripts/Chart.min.js").then(() => {
if (this.get("model") && !this.get("values")) {
this._setPropertiesFromModel(this.get("model"));
this._drawChart();
} else if (this.get("dataSource")) {
this._fetchReport();
}
});
}, },
@computed("dataSourceName") @computed("dataSourceName")
@ -34,10 +49,17 @@ export default Ember.Component.extend({
@computed("trend") @computed("trend")
trendIcon(trend) { trendIcon(trend) {
if (trend === "stable") { switch (trend) {
return null; case "trending-up":
} else { return "angle-up";
return `angle-${trend}`; case "trending-down":
return "angle-down";
case "high-trending-up":
return "angle-double-up";
case "high-trending-down":
return "angle-double-down";
default:
return null;
} }
}, },
@ -46,7 +68,9 @@ export default Ember.Component.extend({
this.set("isLoading", true); this.set("isLoading", true);
let payload = {data: {}}; let payload = {
data: {}
};
if (this.get("startDate")) { if (this.get("startDate")) {
payload.data.start_date = this.get("startDate").toISOString(); payload.data.start_date = this.get("startDate").toISOString();
@ -58,7 +82,7 @@ export default Ember.Component.extend({
ajax(this.get("dataSource"), payload) ajax(this.get("dataSource"), payload)
.then((response) => { .then((response) => {
this._setPropertiesFromModel(response.report); this._setPropertiesFromModel(Report.create(response.report));
}) })
.finally(() => { .finally(() => {
this.set("isLoading", false); this.set("isLoading", false);
@ -71,17 +95,6 @@ export default Ember.Component.extend({
}); });
}, },
_initializeChart() {
loadScript("/javascripts/Chart.min.js").then(() => {
if (this.get("model") && !this.get("values")) {
this._setPropertiesFromModel(this.get("model"));
this._drawChart();
} else if (this.get("dataSource")) {
this._fetchReport();
}
});
},
_drawChart() { _drawChart() {
const $chartCanvas = this.$(".chart-canvas"); const $chartCanvas = this.$(".chart-canvas");
if (!$chartCanvas.length) return; if (!$chartCanvas.length) return;
@ -91,7 +104,7 @@ export default Ember.Component.extend({
const data = { const data = {
labels: this.get("labels"), labels: this.get("labels"),
datasets: [{ datasets: [{
data: this.get("values"), data: Ember.makeArray(this.get("values")),
backgroundColor: this.get("backgroundColor"), backgroundColor: this.get("backgroundColor"),
borderColor: this.get("borderColor") borderColor: this.get("borderColor")
}] }]
@ -100,72 +113,64 @@ export default Ember.Component.extend({
this._chart = new window.Chart(context, this._buildChartConfig(data)); this._chart = new window.Chart(context, this._buildChartConfig(data));
}, },
_setPropertiesFromModel(model) { _setPropertiesFromModel(report) {
const oneDataPoint = (this.get("startDate") && this.get("endDate")) &&
this.get("startDate").isSame(this.get("endDate"), "day");
this.setProperties({ this.setProperties({
labels: model.data.map(r => r.x), oneDataPoint,
values: model.data.map(r => r.y), labels: report.get("data").map(r => r.x),
oneDataPoint: (this.get("startDate") && this.get("endDate")) && values: report.get("data").map(r => r.y),
this.get("startDate").isSame(this.get("endDate"), 'day'), total: report.get("total"),
total: model.total, description: report.get("description"),
title: model.title, title: report.get("title"),
trend: this._computeTrend(model.total, model.prev30Days) trend: report.get("sevenDayTrend"),
prev30Days: report.get("prev30Days"),
}); });
}, },
_buildChartConfig(data) { _buildChartConfig(data) {
const values = this.get("values"); const values = data.datasets[0].data;
const max = Math.max(...values); const max = Math.max(...values);
const min = Math.min(...values); const min = Math.min(...values);
const stepSize = Math.max(...[Math.ceil((max - min)/5), 20]);
const startDate = this.get("startDate") || moment(); const stepSize = Math.max(...[Math.ceil((max - min) / 5) * 5, 20]);
const endDate = this.get("endDate") || moment();
const datesDifference = startDate.diff(endDate, "days");
let unit = "day";
if (datesDifference >= 366) {
unit = "quarter";
} else if (datesDifference >= 61) {
unit = "month";
} else if (datesDifference >= 14) {
unit = "week";
}
return { return {
type: "line", type: "line",
data, data,
options: { options: {
legend: { display: false }, legend: {
display: false
},
responsive: true, responsive: true,
layout: { padding: { left: 0, top: 0, right: 0, bottom: 0 } }, maintainAspectRatio: false,
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0
}
},
scales: { scales: {
yAxes: [ yAxes: [{
{ display: true,
display: true, ticks: {
ticks: { suggestedMin: 0, stepSize, suggestedMax: max + stepSize } suggestedMin: 0,
stepSize,
suggestedMax: max + stepSize
} }
], }],
xAxes: [ xAxes: [{
{ display: true,
display: true, type: "time",
type: "time", time: {
time: { parser: "YYYY-MM-DD"
parser: "YYYY-MM-DD",
unit
}
} }
], }],
} }
}, },
}; };
}, }
_computeTrend(total, prevTotal) {
const percentChange = ((total - prevTotal) / prevTotal) * 100;
if (percentChange > 50) return "double-up";
if (percentChange > 0) return "up";
if (percentChange === 0) return "stable";
if (percentChange < 50) return "double-down";
if (percentChange < 0) return "down";
},
}); });

View File

@ -1,11 +1,6 @@
import DiscourseURL from "discourse/lib/url"; import DiscourseURL from "discourse/lib/url";
import computed from "ember-addons/ember-computed-decorators"; import computed from "ember-addons/ember-computed-decorators";
import AdminDashboardNext from 'admin/models/admin-dashboard-next'; import AdminDashboardNext from 'admin/models/admin-dashboard-next';
import Report from 'admin/models/report';
const ATTRIBUTES = [ "disk_space", "updated_at", "last_backup_taken_at"];
const REPORTS = [ "global_reports", "user_reports" ];
export default Ember.Controller.extend({ export default Ember.Controller.extend({
queryParams: ["period"], queryParams: ["period"],
@ -14,20 +9,17 @@ export default Ember.Controller.extend({
dashboardFetchedAt: null, dashboardFetchedAt: null,
exceptionController: Ember.inject.controller('exception'), exceptionController: Ember.inject.controller('exception'),
diskSpace: Ember.computed.alias("model.attributes.disk_space"),
fetchDashboard() { fetchDashboard() {
if (this.get("isLoading")) return; if (this.get("isLoading")) return;
if (!this.get("dashboardFetchedAt") || moment().subtract(30, "minutes").toDate() > this.get("dashboardFetchedAt")) { if (!this.get("dashboardFetchedAt") || moment().subtract(30, "minutes").toDate() > this.get("dashboardFetchedAt")) {
this.set("isLoading", true); this.set("isLoading", true);
AdminDashboardNext.find().then(d => { AdminDashboardNext.find().then(adminDashboardNextModel => {
this.set("dashboardFetchedAt", new Date()); this.set("dashboardFetchedAt", new Date());
this.set("model", adminDashboardNextModel);
const reports = {};
REPORTS.forEach(name => d[name].forEach(r => reports[`${name}_${r.type}`] = Report.create(r)));
this.setProperties(reports);
ATTRIBUTES.forEach(a => this.set(a, d[a]));
}).catch(e => { }).catch(e => {
this.get("exceptionController").set("thrown", e.jqXHR); this.get("exceptionController").set("thrown", e.jqXHR);
this.replaceRoute("exception"); this.replaceRoute("exception");

View File

@ -1,9 +1,13 @@
import { ajax } from 'discourse/lib/ajax'; import { ajax } from "discourse/lib/ajax";
import Report from "admin/models/report";
const ATTRIBUTES = [ "disk_space", "updated_at", "last_backup_taken_at"];
const REPORTS = [ "global_reports", "user_reports" ];
const AdminDashboardNext = Discourse.Model.extend({}); const AdminDashboardNext = Discourse.Model.extend({});
AdminDashboardNext.reopenClass({ AdminDashboardNext.reopenClass({
/** /**
Fetch all dashboard data. This can be an expensive request when the cached data Fetch all dashboard data. This can be an expensive request when the cached data
has expired and the server must collect the data again. has expired and the server must collect the data again.
@ -11,13 +15,26 @@ AdminDashboardNext.reopenClass({
@method find @method find
@return {jqXHR} a jQuery Promise object @return {jqXHR} a jQuery Promise object
**/ **/
find: function() { find() {
return ajax("/admin/dashboard-next.json").then(function(json) { return ajax("/admin/dashboard-next.json").then(function(json) {
var model = AdminDashboardNext.create(json); var model = AdminDashboardNext.create();
model.set('loaded', true);
const reports = {};
REPORTS.forEach(name => json[name].forEach(r => {
if (!reports[name]) reports[name] = {};
reports[name][r.type] = Report.create(r);
}));
model.set("reports", reports);
const attributes = {};
ATTRIBUTES.forEach(a => attributes[a] = json[a]);
model.set("attributes", attributes);
model.set("loaded", true);
return model; return model;
}); });
}, }
}); });
export default AdminDashboardNext; export default AdminDashboardNext;

View File

@ -60,12 +60,18 @@ const Report = Discourse.Model.extend({
sevenDayTrend() { sevenDayTrend() {
const currentPeriod = this.valueFor(1, 7); const currentPeriod = this.valueFor(1, 7);
const prevPeriod = this.valueFor(8, 14); const prevPeriod = this.valueFor(8, 14);
if (currentPeriod > prevPeriod) { const change = ((currentPeriod - prevPeriod) / prevPeriod) * 100;
if (change > 50) {
return "high-trending-up";
} else if (change > 0) {
return "trending-up"; return "trending-up";
} else if (currentPeriod < prevPeriod) { } else if (change === 0) {
return "trending-down";
} else {
return "no-change"; return "no-change";
} else if (change < -50) {
return "high-trending-down";
} else if (change < 0) {
return "trending-down";
} }
}, },

View File

@ -2,8 +2,10 @@
<div class="chart-title"> <div class="chart-title">
<h3>{{title}}</h3> <h3>{{title}}</h3>
{{#if help}} {{#if description}}
{{d-icon "question-circle" title=help}} <span title={{description}}>
{{d-icon "question-circle"}}
</span>
{{/if}} {{/if}}
</div> </div>
@ -14,7 +16,7 @@
</span> </span>
{{else}} {{else}}
<div class="chart-trend {{trend}}"> <div class="chart-trend {{trend}}">
<span>{{number total}}</span> <span>{{number prev30Days}}</span>
{{#if trendIcon}} {{#if trendIcon}}
{{d-icon trendIcon}} {{d-icon trendIcon}}

View File

@ -1,5 +1,5 @@
{{plugin-outlet name="admin-dashboard-top"}} {{plugin-outlet name="admin-dashboard-top"}}
{{lastRefreshedAt}}
<div class="community-health section"> <div class="community-health section">
<div class="section-title"> <div class="section-title">
<h2>{{i18n "admin.dashboard.community_health"}}</h2> <h2>{{i18n "admin.dashboard.community_health"}}</h2>
@ -9,31 +9,61 @@
<div class="section-body"> <div class="section-body">
<div class="charts"> <div class="charts">
{{dashboard-mini-chart {{dashboard-mini-chart
model=global_reports_signups model=model.reports.global_reports.signups
dataSourceName="signups" dataSourceName="signups"
startDate=startDate startDate=startDate
endDate=endDate endDate=endDate}}
help="admin.dashboard.charts.signups.help"}}
{{dashboard-mini-chart {{dashboard-mini-chart
model=global_reports_topics model=model.reports.global_reports.topics
dataSourceName="topics" dataSourceName="topics"
startDate=startDate startDate=startDate
endDate=endDate endDate=endDate}}
help="admin.dashboard.charts.topics.help"}}
{{dashboard-mini-chart
model=model.reports.global_reports.new_contributors
dataSourceName="new_contributors"
startDate=startDate
endDate=endDate}}
</div> </div>
</div> </div>
</div> </div>
<div class="section-columns"> <div class="section-columns">
<div class="section-column"> <div class="section-column">
<div class="dashboard-table">
<div class="table-title">
<h3>{{i18n "admin.dashboard.activity_metrics"}}</h3>
</div>
<div class="table-container">
<table>
<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>
<th>{{i18n 'admin.dashboard.reports.all'}}</th>
</tr>
</thead>
<tbody>
{{admin-report-counts report=model.reports.global_reports.topics}}
{{admin-report-counts report=model.reports.global_reports.signups}}
{{admin-report-counts report=model.reports.global_reports.new_contributors}}
</tbody>
</table>
</div>
</div>
{{dashboard-inline-table {{dashboard-inline-table
model=user_reports_users_by_type model=model.reports.user_reports.users_by_type
lastRefreshedAt=lastRefreshedAt lastRefreshedAt=lastRefreshedAt
isLoading=isLoading}} isLoading=isLoading}}
{{dashboard-inline-table {{dashboard-inline-table
model=user_reports_users_by_trust_level model=model.reports.user_reports.users_by_trust_level
lastRefreshedAt=lastRefreshedAt lastRefreshedAt=lastRefreshedAt
isLoading=isLoading}} isLoading=isLoading}}
@ -44,7 +74,7 @@
<div class="backups"> <div class="backups">
<h3 class="durability-title"><a href="/admin/backups">{{i18n "admin.dashboard.backups"}}</a></h3> <h3 class="durability-title"><a href="/admin/backups">{{i18n "admin.dashboard.backups"}}</a></h3>
<p> <p>
{{disk_space.backups_used}} ({{i18n "admin.dashboard.space_free" size=disk_space.backups_free}}) {{diskSpace.backups_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.backups_free}})
<br /> <br />
{{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}} {{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}
</p> </p>
@ -54,7 +84,7 @@
<div class="uploads"> <div class="uploads">
<h3 class="durability-title">{{i18n "admin.dashboard.uploads"}}</h3> <h3 class="durability-title">{{i18n "admin.dashboard.uploads"}}</h3>
<p> <p>
{{disk_space.uploads_used}} ({{i18n "admin.dashboard.space_free" size=disk_space.uploads_free}}) {{diskSpace.uploads_used}} ({{i18n "admin.dashboard.space_free" size=diskSpace.uploads_free}})
</p> </p>
</div> </div>
</div> </div>

View File

@ -965,13 +965,13 @@ table.api-keys {
display: none; display: none;
} }
&.trending-up { &.high-trending-up, &.trending-up {
i.up { i.up {
color: $success; color: $success;
display: inline; display: inline;
} }
} }
&.trending-down { &.high-trending-down, &.trending-down {
i.down { i.down {
color: $danger; color: $danger;
display: inline; display: inline;
@ -986,10 +986,10 @@ table.api-keys {
} }
tr.reverse-colors { tr.reverse-colors {
td.value.trending-down i.down { td.value.high-trending-down i.down, td.value.trending-down i.down {
color: $success; color: $success;
} }
td.value.trending-up i.up { td.value.high-trending-up i.up, td.value.trending-up i.up {
color: $danger; color: $danger;
} }
} }

View File

@ -43,6 +43,10 @@
.dashboard-table { .dashboard-table {
margin-bottom: 1em; margin-bottom: 1em;
&.fixed table {
table-layout: fixed;
}
&.is-loading { &.is-loading {
height: 150px; height: 150px;
} }
@ -59,7 +63,6 @@
table { table {
border: 1px solid $primary-low-mid; border: 1px solid $primary-low-mid;
table-layout: fixed;
thead { thead {
tr { tr {
@ -67,6 +70,10 @@
th { th {
border: 1px solid $primary-low-mid; border: 1px solid $primary-low-mid;
text-align: center; text-align: center;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
} }
} }
} }
@ -77,6 +84,31 @@
border: 1px solid $primary-low-mid; border: 1px solid $primary-low-mid;
text-align: center; text-align: center;
} }
td.value {
i {
display: none;
}
&.high-trending-up, &.trending-up {
i.up {
color: $success;
display: inline;
}
}
&.high-trending-down, &.trending-down {
i.down {
color: $danger;
display: inline;
}
}
&.no-change {
i.down {
display: inline;
visibility: hidden;
}
}
}
} }
} }
} }
@ -110,13 +142,13 @@
} }
} }
&.double-up, &.up { &.high-trending-up, &.trending-up {
.chart-trend, .data-point { .chart-trend, .data-point {
color: rgb(17, 141, 0); color: rgb(17, 141, 0);
} }
} }
&.double-down, &.down { &.high-trending-down, &.trending-down {
.chart-trend, .data-point { .chart-trend, .data-point {
color: $danger; color: $danger;
} }
@ -145,13 +177,14 @@
.chart-container { .chart-container {
position: relative; position: relative;
padding: 0 1em; padding: 0 1em;
min-height: 200px;
} }
.chart-trend { .chart-trend {
font-size: $font-up-5; font-size: $font-up-5;
position: absolute; position: absolute;
right: 1.5em; right: 40px;
top: .5em; top: 5px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;

View File

@ -4,7 +4,8 @@ class AdminDashboardNextData
GLOBAL_REPORTS ||= [ GLOBAL_REPORTS ||= [
'signups', 'signups',
'topics', 'topics',
'trending_search' 'trending_search',
'new_contributors'
] ]
USER_REPORTS ||= [ USER_REPORTS ||= [

View File

@ -20,6 +20,7 @@ class Report
title: I18n.t("reports.#{type}.title"), title: I18n.t("reports.#{type}.title"),
xaxis: I18n.t("reports.#{type}.xaxis"), xaxis: I18n.t("reports.#{type}.xaxis"),
yaxis: I18n.t("reports.#{type}.yaxis"), yaxis: I18n.t("reports.#{type}.yaxis"),
description: I18n.t("reports.#{type}.description"),
data: data, data: data,
total: total, total: total,
start_date: start_date, start_date: start_date,
@ -109,6 +110,10 @@ class Report
end end
end end
def self.report_new_contributors(report)
report_about report, User.real, :count_by_first_post
end
def self.report_profile_views(report) def self.report_profile_views(report)
start_date = report.start_date.to_date start_date = report.start_date.to_date
end_date = report.end_date.to_date end_date = report.end_date.to_date

View File

@ -21,6 +21,7 @@ class Topic < ActiveRecord::Base
include Searchable include Searchable
include LimitedEdit include LimitedEdit
extend Forwardable extend Forwardable
include DateGroupable
def_delegator :featured_users, :user_ids, :featured_user_ids def_delegator :featured_users, :user_ids, :featured_user_ids
def_delegator :featured_users, :choose, :feature_topic_users def_delegator :featured_users, :choose, :feature_topic_users
@ -458,9 +459,9 @@ class Topic < ActiveRecord::Base
end end
def self.listable_count_per_day(start_date, end_date, category_id = nil) def self.listable_count_per_day(start_date, end_date, category_id = nil)
result = listable_topics.where('created_at >= ? and created_at <= ?', start_date, end_date) result = listable_topics.smart_group_by_date("topics.created_at", start_date, end_date)
result = result.where(category_id: category_id) if category_id result = result.where(category_id: category_id) if category_id
result.group('date(created_at)').order('date(created_at)').count result.count
end end
def private_message? def private_message?

View File

@ -19,6 +19,7 @@ class User < ActiveRecord::Base
include Roleable include Roleable
include HasCustomFields include HasCustomFields
include SecondFactorManager include SecondFactorManager
include DateGroupable
# TODO: Remove this after 7th Jan 2018 # TODO: Remove this after 7th Jan 2018
self.ignored_columns = %w{email} self.ignored_columns = %w{email}
@ -829,13 +830,20 @@ class User < ActiveRecord::Base
end end
def self.count_by_signup_date(start_date, end_date, group_id = nil) def self.count_by_signup_date(start_date, end_date, group_id = nil)
result = where('users.created_at >= ? AND users.created_at <= ?', start_date, end_date) result = smart_group_by_date("users.created_at", start_date, end_date)
if group_id if group_id
result = result.joins("INNER JOIN group_users ON group_users.user_id = users.id") result = result.joins("INNER JOIN group_users ON group_users.user_id = users.id")
result = result.where("group_users.group_id = ?", group_id) result = result.where("group_users.group_id = ?", group_id)
end end
result.group('date(users.created_at)').order('date(users.created_at)').count
result.count
end
def self.count_by_first_post(start_date, end_date)
joins('INNER JOIN user_stats AS us ON us.user_id = users.id')
.smart_group_by_date("us.first_post_created_at", start_date, end_date)
.count
end end
def secure_category_ids def secure_category_ids

View File

@ -2742,12 +2742,7 @@ en:
show_traffic_report: "Show Detailed Traffic Report" show_traffic_report: "Show Detailed Traffic Report"
community_health: Community health community_health: Community health
whats_new_in_discourse: Whats new in Discourse? whats_new_in_discourse: Whats new in Discourse?
activity_metrics: Activity Metrics
charts:
signups:
help: Users created for this period
topics:
help: Topics created for this period
reports: reports:
today: "Today" today: "Today"

View File

@ -840,6 +840,12 @@ en:
title: "New Users" title: "New Users"
xaxis: "Day" xaxis: "Day"
yaxis: "Number of new users" yaxis: "Number of new users"
description: "Users created for this period"
new_contributors:
title: "New Contributors"
xaxis: "Day"
yaxis: "Number of new contributors"
description: "Number of users who made their first contribution"
profile_views: profile_views:
title: "User Profile Views" title: "User Profile Views"
xaxis: "Day" xaxis: "Day"
@ -848,6 +854,7 @@ en:
title: "Topics" title: "Topics"
xaxis: "Day" xaxis: "Day"
yaxis: "Number of new topics" yaxis: "Number of new topics"
description: "Topics created for this period"
posts: posts:
title: "Posts" title: "Posts"
xaxis: "Day" xaxis: "Day"

View File

@ -250,6 +250,36 @@ describe Report do
end end
end end
describe 'new contributors report' do
let(:report) { Report.find('new_contributors') }
context "no contributors" do
it "returns an empty report" do
expect(report.data).to be_blank
end
end
context "with contributors" do
before do
jeff = Fabricate(:user)
jeff.user_stat = UserStat.new(new_since: 1.hour.ago, first_post_created_at: 1.day.ago)
regis = Fabricate(:user)
regis.user_stat = UserStat.new(new_since: 1.hour.ago, first_post_created_at: 2.days.ago)
hawk = Fabricate(:user)
hawk.user_stat = UserStat.new(new_since: 1.hour.ago, first_post_created_at: 2.days.ago)
end
it "returns a report with data" do
expect(report.data).to be_present
expect(report.data[0][:y]).to eq 2
expect(report.data[1][:y]).to eq 1
end
end
end
describe 'users by types level report' do describe 'users by types level report' do
let(:report) { Report.find('users_by_type') } let(:report) { Report.find('users_by_type') }