dashboard next: activity metrics and new contributors
This commit also introduces a better grouping of data points.
This commit is contained in:
parent
b26e27bdab
commit
9fabf2543b
|
@ -2,7 +2,7 @@ import { ajax } from 'discourse/lib/ajax';
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["dashboard-table", "dashboard-inline-table"],
|
||||
classNames: ["dashboard-table", "dashboard-inline-table", "fixed"],
|
||||
|
||||
classNameBindings: ["isLoading"],
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ajax } from 'discourse/lib/ajax';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import loadScript from 'discourse/lib/load-script';
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
import Report from "admin/models/report";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["dashboard-mini-chart"],
|
||||
|
@ -17,12 +18,26 @@ export default Ember.Component.extend({
|
|||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
this._initializeChart();
|
||||
|
||||
if (this.get("model")) {
|
||||
loadScript("/javascripts/Chart.min.js").then(() => {
|
||||
this._setPropertiesFromModel(this.get("model"));
|
||||
this._drawChart();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
didUpdateAttrs() {
|
||||
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")
|
||||
|
@ -34,10 +49,17 @@ export default Ember.Component.extend({
|
|||
|
||||
@computed("trend")
|
||||
trendIcon(trend) {
|
||||
if (trend === "stable") {
|
||||
return null;
|
||||
} else {
|
||||
return `angle-${trend}`;
|
||||
switch (trend) {
|
||||
case "trending-up":
|
||||
return "angle-up";
|
||||
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);
|
||||
|
||||
let payload = {data: {}};
|
||||
let payload = {
|
||||
data: {}
|
||||
};
|
||||
|
||||
if (this.get("startDate")) {
|
||||
payload.data.start_date = this.get("startDate").toISOString();
|
||||
|
@ -58,7 +82,7 @@ export default Ember.Component.extend({
|
|||
|
||||
ajax(this.get("dataSource"), payload)
|
||||
.then((response) => {
|
||||
this._setPropertiesFromModel(response.report);
|
||||
this._setPropertiesFromModel(Report.create(response.report));
|
||||
})
|
||||
.finally(() => {
|
||||
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() {
|
||||
const $chartCanvas = this.$(".chart-canvas");
|
||||
if (!$chartCanvas.length) return;
|
||||
|
@ -91,7 +104,7 @@ export default Ember.Component.extend({
|
|||
const data = {
|
||||
labels: this.get("labels"),
|
||||
datasets: [{
|
||||
data: this.get("values"),
|
||||
data: Ember.makeArray(this.get("values")),
|
||||
backgroundColor: this.get("backgroundColor"),
|
||||
borderColor: this.get("borderColor")
|
||||
}]
|
||||
|
@ -100,72 +113,64 @@ export default Ember.Component.extend({
|
|||
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({
|
||||
labels: model.data.map(r => r.x),
|
||||
values: model.data.map(r => r.y),
|
||||
oneDataPoint: (this.get("startDate") && this.get("endDate")) &&
|
||||
this.get("startDate").isSame(this.get("endDate"), 'day'),
|
||||
total: model.total,
|
||||
title: model.title,
|
||||
trend: this._computeTrend(model.total, model.prev30Days)
|
||||
oneDataPoint,
|
||||
labels: report.get("data").map(r => r.x),
|
||||
values: report.get("data").map(r => r.y),
|
||||
total: report.get("total"),
|
||||
description: report.get("description"),
|
||||
title: report.get("title"),
|
||||
trend: report.get("sevenDayTrend"),
|
||||
prev30Days: report.get("prev30Days"),
|
||||
});
|
||||
},
|
||||
|
||||
_buildChartConfig(data) {
|
||||
const values = this.get("values");
|
||||
const values = data.datasets[0].data;
|
||||
const max = Math.max(...values);
|
||||
const min = Math.min(...values);
|
||||
const stepSize = Math.max(...[Math.ceil((max - min)/5), 20]);
|
||||
|
||||
const startDate = this.get("startDate") || moment();
|
||||
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";
|
||||
}
|
||||
const stepSize = Math.max(...[Math.ceil((max - min) / 5) * 5, 20]);
|
||||
|
||||
return {
|
||||
type: "line",
|
||||
data,
|
||||
options: {
|
||||
legend: { display: false },
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
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: {
|
||||
yAxes: [
|
||||
{
|
||||
display: true,
|
||||
ticks: { suggestedMin: 0, stepSize, suggestedMax: max + stepSize }
|
||||
yAxes: [{
|
||||
display: true,
|
||||
ticks: {
|
||||
suggestedMin: 0,
|
||||
stepSize,
|
||||
suggestedMax: max + stepSize
|
||||
}
|
||||
],
|
||||
xAxes: [
|
||||
{
|
||||
display: true,
|
||||
type: "time",
|
||||
time: {
|
||||
parser: "YYYY-MM-DD",
|
||||
unit
|
||||
}
|
||||
}],
|
||||
xAxes: [{
|
||||
display: true,
|
||||
type: "time",
|
||||
time: {
|
||||
parser: "YYYY-MM-DD"
|
||||
}
|
||||
],
|
||||
}],
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
_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";
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import DiscourseURL from "discourse/lib/url";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
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({
|
||||
queryParams: ["period"],
|
||||
|
@ -14,20 +9,17 @@ export default Ember.Controller.extend({
|
|||
dashboardFetchedAt: null,
|
||||
exceptionController: Ember.inject.controller('exception'),
|
||||
|
||||
diskSpace: Ember.computed.alias("model.attributes.disk_space"),
|
||||
|
||||
fetchDashboard() {
|
||||
if (this.get("isLoading")) return;
|
||||
|
||||
if (!this.get("dashboardFetchedAt") || moment().subtract(30, "minutes").toDate() > this.get("dashboardFetchedAt")) {
|
||||
this.set("isLoading", true);
|
||||
|
||||
AdminDashboardNext.find().then(d => {
|
||||
AdminDashboardNext.find().then(adminDashboardNextModel => {
|
||||
this.set("dashboardFetchedAt", new Date());
|
||||
|
||||
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]));
|
||||
this.set("model", adminDashboardNextModel);
|
||||
}).catch(e => {
|
||||
this.get("exceptionController").set("thrown", e.jqXHR);
|
||||
this.replaceRoute("exception");
|
||||
|
|
|
@ -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({});
|
||||
|
||||
AdminDashboardNext.reopenClass({
|
||||
|
||||
/**
|
||||
Fetch all dashboard data. This can be an expensive request when the cached data
|
||||
has expired and the server must collect the data again.
|
||||
|
@ -11,13 +15,26 @@ AdminDashboardNext.reopenClass({
|
|||
@method find
|
||||
@return {jqXHR} a jQuery Promise object
|
||||
**/
|
||||
find: function() {
|
||||
find() {
|
||||
return ajax("/admin/dashboard-next.json").then(function(json) {
|
||||
var model = AdminDashboardNext.create(json);
|
||||
model.set('loaded', true);
|
||||
var model = AdminDashboardNext.create();
|
||||
|
||||
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;
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export default AdminDashboardNext;
|
||||
|
|
|
@ -60,12 +60,18 @@ const Report = Discourse.Model.extend({
|
|||
sevenDayTrend() {
|
||||
const currentPeriod = this.valueFor(1, 7);
|
||||
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";
|
||||
} else if (currentPeriod < prevPeriod) {
|
||||
return "trending-down";
|
||||
} else {
|
||||
} else if (change === 0) {
|
||||
return "no-change";
|
||||
} else if (change < -50) {
|
||||
return "high-trending-down";
|
||||
} else if (change < 0) {
|
||||
return "trending-down";
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
<div class="chart-title">
|
||||
<h3>{{title}}</h3>
|
||||
|
||||
{{#if help}}
|
||||
{{d-icon "question-circle" title=help}}
|
||||
{{#if description}}
|
||||
<span title={{description}}>
|
||||
{{d-icon "question-circle"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
|
@ -14,7 +16,7 @@
|
|||
</span>
|
||||
{{else}}
|
||||
<div class="chart-trend {{trend}}">
|
||||
<span>{{number total}}</span>
|
||||
<span>{{number prev30Days}}</span>
|
||||
|
||||
{{#if trendIcon}}
|
||||
{{d-icon trendIcon}}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{{plugin-outlet name="admin-dashboard-top"}}
|
||||
{{lastRefreshedAt}}
|
||||
|
||||
<div class="community-health section">
|
||||
<div class="section-title">
|
||||
<h2>{{i18n "admin.dashboard.community_health"}}</h2>
|
||||
|
@ -9,31 +9,61 @@
|
|||
<div class="section-body">
|
||||
<div class="charts">
|
||||
{{dashboard-mini-chart
|
||||
model=global_reports_signups
|
||||
model=model.reports.global_reports.signups
|
||||
dataSourceName="signups"
|
||||
startDate=startDate
|
||||
endDate=endDate
|
||||
help="admin.dashboard.charts.signups.help"}}
|
||||
endDate=endDate}}
|
||||
|
||||
{{dashboard-mini-chart
|
||||
model=global_reports_topics
|
||||
model=model.reports.global_reports.topics
|
||||
dataSourceName="topics"
|
||||
startDate=startDate
|
||||
endDate=endDate
|
||||
help="admin.dashboard.charts.topics.help"}}
|
||||
endDate=endDate}}
|
||||
|
||||
{{dashboard-mini-chart
|
||||
model=model.reports.global_reports.new_contributors
|
||||
dataSourceName="new_contributors"
|
||||
startDate=startDate
|
||||
endDate=endDate}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-columns">
|
||||
<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
|
||||
model=user_reports_users_by_type
|
||||
model=model.reports.user_reports.users_by_type
|
||||
lastRefreshedAt=lastRefreshedAt
|
||||
isLoading=isLoading}}
|
||||
|
||||
{{dashboard-inline-table
|
||||
model=user_reports_users_by_trust_level
|
||||
model=model.reports.user_reports.users_by_trust_level
|
||||
lastRefreshedAt=lastRefreshedAt
|
||||
isLoading=isLoading}}
|
||||
|
||||
|
@ -44,7 +74,7 @@
|
|||
<div class="backups">
|
||||
<h3 class="durability-title"><a href="/admin/backups">{{i18n "admin.dashboard.backups"}}</a></h3>
|
||||
<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 />
|
||||
{{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}
|
||||
</p>
|
||||
|
@ -54,7 +84,7 @@
|
|||
<div class="uploads">
|
||||
<h3 class="durability-title">{{i18n "admin.dashboard.uploads"}}</h3>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -965,13 +965,13 @@ table.api-keys {
|
|||
display: none;
|
||||
}
|
||||
|
||||
&.trending-up {
|
||||
&.high-trending-up, &.trending-up {
|
||||
i.up {
|
||||
color: $success;
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
&.trending-down {
|
||||
&.high-trending-down, &.trending-down {
|
||||
i.down {
|
||||
color: $danger;
|
||||
display: inline;
|
||||
|
@ -986,10 +986,10 @@ table.api-keys {
|
|||
}
|
||||
|
||||
tr.reverse-colors {
|
||||
td.value.trending-down i.down {
|
||||
td.value.high-trending-down i.down, td.value.trending-down i.down {
|
||||
color: $success;
|
||||
}
|
||||
td.value.trending-up i.up {
|
||||
td.value.high-trending-up i.up, td.value.trending-up i.up {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,10 @@
|
|||
.dashboard-table {
|
||||
margin-bottom: 1em;
|
||||
|
||||
&.fixed table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
height: 150px;
|
||||
}
|
||||
|
@ -59,7 +63,6 @@
|
|||
|
||||
table {
|
||||
border: 1px solid $primary-low-mid;
|
||||
table-layout: fixed;
|
||||
|
||||
thead {
|
||||
tr {
|
||||
|
@ -67,6 +70,10 @@
|
|||
th {
|
||||
border: 1px solid $primary-low-mid;
|
||||
text-align: center;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +84,31 @@
|
|||
border: 1px solid $primary-low-mid;
|
||||
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 {
|
||||
color: rgb(17, 141, 0);
|
||||
}
|
||||
}
|
||||
|
||||
&.double-down, &.down {
|
||||
&.high-trending-down, &.trending-down {
|
||||
.chart-trend, .data-point {
|
||||
color: $danger;
|
||||
}
|
||||
|
@ -145,13 +177,14 @@
|
|||
.chart-container {
|
||||
position: relative;
|
||||
padding: 0 1em;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.chart-trend {
|
||||
font-size: $font-up-5;
|
||||
position: absolute;
|
||||
right: 1.5em;
|
||||
top: .5em;
|
||||
right: 40px;
|
||||
top: 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
|
|
@ -4,7 +4,8 @@ class AdminDashboardNextData
|
|||
GLOBAL_REPORTS ||= [
|
||||
'signups',
|
||||
'topics',
|
||||
'trending_search'
|
||||
'trending_search',
|
||||
'new_contributors'
|
||||
]
|
||||
|
||||
USER_REPORTS ||= [
|
||||
|
|
|
@ -20,6 +20,7 @@ class Report
|
|||
title: I18n.t("reports.#{type}.title"),
|
||||
xaxis: I18n.t("reports.#{type}.xaxis"),
|
||||
yaxis: I18n.t("reports.#{type}.yaxis"),
|
||||
description: I18n.t("reports.#{type}.description"),
|
||||
data: data,
|
||||
total: total,
|
||||
start_date: start_date,
|
||||
|
@ -109,6 +110,10 @@ class Report
|
|||
end
|
||||
end
|
||||
|
||||
def self.report_new_contributors(report)
|
||||
report_about report, User.real, :count_by_first_post
|
||||
end
|
||||
|
||||
def self.report_profile_views(report)
|
||||
start_date = report.start_date.to_date
|
||||
end_date = report.end_date.to_date
|
||||
|
|
|
@ -21,6 +21,7 @@ class Topic < ActiveRecord::Base
|
|||
include Searchable
|
||||
include LimitedEdit
|
||||
extend Forwardable
|
||||
include DateGroupable
|
||||
|
||||
def_delegator :featured_users, :user_ids, :featured_user_ids
|
||||
def_delegator :featured_users, :choose, :feature_topic_users
|
||||
|
@ -458,9 +459,9 @@ class Topic < ActiveRecord::Base
|
|||
end
|
||||
|
||||
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.group('date(created_at)').order('date(created_at)').count
|
||||
result.count
|
||||
end
|
||||
|
||||
def private_message?
|
||||
|
|
|
@ -19,6 +19,7 @@ class User < ActiveRecord::Base
|
|||
include Roleable
|
||||
include HasCustomFields
|
||||
include SecondFactorManager
|
||||
include DateGroupable
|
||||
|
||||
# TODO: Remove this after 7th Jan 2018
|
||||
self.ignored_columns = %w{email}
|
||||
|
@ -829,13 +830,20 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
|
||||
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
|
||||
result = result.joins("INNER JOIN group_users ON group_users.user_id = users.id")
|
||||
result = result.where("group_users.group_id = ?", group_id)
|
||||
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
|
||||
|
||||
def secure_category_ids
|
||||
|
|
|
@ -2742,12 +2742,7 @@ en:
|
|||
show_traffic_report: "Show Detailed Traffic Report"
|
||||
community_health: Community health
|
||||
whats_new_in_discourse: What’s new in Discourse?
|
||||
|
||||
charts:
|
||||
signups:
|
||||
help: Users created for this period
|
||||
topics:
|
||||
help: Topics created for this period
|
||||
activity_metrics: Activity Metrics
|
||||
|
||||
reports:
|
||||
today: "Today"
|
||||
|
|
|
@ -840,6 +840,12 @@ en:
|
|||
title: "New Users"
|
||||
xaxis: "Day"
|
||||
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:
|
||||
title: "User Profile Views"
|
||||
xaxis: "Day"
|
||||
|
@ -848,6 +854,7 @@ en:
|
|||
title: "Topics"
|
||||
xaxis: "Day"
|
||||
yaxis: "Number of new topics"
|
||||
description: "Topics created for this period"
|
||||
posts:
|
||||
title: "Posts"
|
||||
xaxis: "Day"
|
||||
|
|
|
@ -250,6 +250,36 @@ describe Report do
|
|||
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
|
||||
let(:report) { Report.find('users_by_type') }
|
||||
|
||||
|
|
Loading…
Reference in New Issue