mirror of
https://github.com/discourse/discourse.git
synced 2025-05-23 17:01:09 +08:00
dashboard next: trending search report
This commit also improves how data is loaded sync and async
This commit is contained in:
@ -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-mini-table"],
|
classNames: ["dashboard-table", "dashboard-inline-table"],
|
||||||
|
|
||||||
classNameBindings: ["isLoading"],
|
classNameBindings: ["isLoading"],
|
||||||
|
|
@ -11,22 +11,18 @@ export default Ember.Component.extend({
|
|||||||
total: null,
|
total: null,
|
||||||
trend: null,
|
trend: null,
|
||||||
title: null,
|
title: null,
|
||||||
chartData: null,
|
|
||||||
oneDataPoint: false,
|
oneDataPoint: false,
|
||||||
backgroundColor: "rgba(200,220,240,0.3)",
|
backgroundColor: "rgba(200,220,240,0.3)",
|
||||||
borderColor: "#08C",
|
borderColor: "#08C",
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super();
|
||||||
|
this._initializeChart();
|
||||||
|
},
|
||||||
|
|
||||||
didUpdateAttrs() {
|
didUpdateAttrs() {
|
||||||
this._super();
|
this._super();
|
||||||
|
this._initializeChart();
|
||||||
loadScript("/javascripts/Chart.min.js").then(() => {
|
|
||||||
if (this.get("model") && !this.get("chartData")) {
|
|
||||||
this._setPropertiesFromModel(this.get("model"));
|
|
||||||
this._drawChart();
|
|
||||||
} else if (this.get("dataSource")) {
|
|
||||||
this._fetchReport();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@computed("dataSourceName")
|
@computed("dataSourceName")
|
||||||
@ -75,13 +71,27 @@ 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 context = this.$(".chart-canvas")[0].getContext("2d");
|
const $chartCanvas = this.$(".chart-canvas");
|
||||||
|
if (!$chartCanvas.length) return;
|
||||||
|
|
||||||
|
const context = $chartCanvas[0].getContext("2d");
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
labels: this.get("chartData").map(r => r.x),
|
labels: this.get("labels"),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: this.get("chartData").map(r => r.y),
|
data: this.get("values"),
|
||||||
backgroundColor: this.get("backgroundColor"),
|
backgroundColor: this.get("backgroundColor"),
|
||||||
borderColor: this.get("borderColor")
|
borderColor: this.get("borderColor")
|
||||||
}]
|
}]
|
||||||
@ -92,17 +102,18 @@ export default Ember.Component.extend({
|
|||||||
|
|
||||||
_setPropertiesFromModel(model) {
|
_setPropertiesFromModel(model) {
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
|
labels: model.data.map(r => r.x),
|
||||||
|
values: model.data.map(r => r.y),
|
||||||
oneDataPoint: (this.get("startDate") && this.get("endDate")) &&
|
oneDataPoint: (this.get("startDate") && this.get("endDate")) &&
|
||||||
this.get("startDate").isSame(this.get("endDate"), 'day'),
|
this.get("startDate").isSame(this.get("endDate"), 'day'),
|
||||||
total: model.total,
|
total: model.total,
|
||||||
title: model.title,
|
title: model.title,
|
||||||
trend: this._computeTrend(model.total, model.prev30Days),
|
trend: this._computeTrend(model.total, model.prev30Days)
|
||||||
chartData: model.data
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_buildChartConfig(data) {
|
_buildChartConfig(data) {
|
||||||
const values = this.get("chartData").map(d => d.y);
|
const values = this.get("values");
|
||||||
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 stepSize = Math.max(...[Math.ceil((max - min)/5), 20]);
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import DashboardTable from "admin/components/dashboard-table";
|
||||||
|
import { number } from 'discourse/lib/formatter';
|
||||||
|
|
||||||
|
export default DashboardTable.extend({
|
||||||
|
layoutName: "admin/templates/components/dashboard-table",
|
||||||
|
|
||||||
|
classNames: ["dashboard-table", "dashboard-table-trending-search"],
|
||||||
|
|
||||||
|
transformModel(model) {
|
||||||
|
return {
|
||||||
|
labels: model.labels,
|
||||||
|
values: model.data.map(data => {
|
||||||
|
return [data[0], number(data[1]), number(data[2])];
|
||||||
|
})
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
@ -0,0 +1,83 @@
|
|||||||
|
import { ajax } from 'discourse/lib/ajax';
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
classNames: ["dashboard-table"],
|
||||||
|
|
||||||
|
classNameBindings: ["isLoading"],
|
||||||
|
|
||||||
|
total: null,
|
||||||
|
labels: null,
|
||||||
|
title: null,
|
||||||
|
chartData: null,
|
||||||
|
isLoading: false,
|
||||||
|
help: null,
|
||||||
|
helpPage: null,
|
||||||
|
model: null,
|
||||||
|
|
||||||
|
transformModel(model) {
|
||||||
|
const data = model.data.sort((a, b) => a.x >= b.x);
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: model.labels,
|
||||||
|
values: data
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super();
|
||||||
|
this._initializeTable();
|
||||||
|
},
|
||||||
|
|
||||||
|
didUpdateAttrs() {
|
||||||
|
this._super();
|
||||||
|
this._initializeTable();
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("dataSourceName")
|
||||||
|
dataSource(dataSourceName) {
|
||||||
|
return `/admin/reports/${dataSourceName}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
_initializeTable() {
|
||||||
|
if (this.get("model") && !this.get("values")) {
|
||||||
|
this._setPropertiesFromModel(this.get("model"));
|
||||||
|
} else if (this.get("dataSource")) {
|
||||||
|
this._fetchReport();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_fetchReport() {
|
||||||
|
if (this.get("isLoading")) return;
|
||||||
|
|
||||||
|
this.set("isLoading", true);
|
||||||
|
|
||||||
|
let payload = {data: {}};
|
||||||
|
|
||||||
|
if (this.get("startDate")) {
|
||||||
|
payload.data.start_date = this.get("startDate").toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.get("endDate")) {
|
||||||
|
payload.data.end_date = this.get("endDate").toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
ajax(this.get("dataSource"), payload)
|
||||||
|
.then((response) => {
|
||||||
|
this._setPropertiesFromModel(response.report);
|
||||||
|
}).finally(() => {
|
||||||
|
this.set("isLoading", false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_setPropertiesFromModel(model) {
|
||||||
|
const { labels, values } = this.transformModel(model);
|
||||||
|
|
||||||
|
this.setProperties({
|
||||||
|
labels,
|
||||||
|
values,
|
||||||
|
total: model.total,
|
||||||
|
title: model.title
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -34,7 +34,7 @@ export default Ember.Controller.extend({
|
|||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.set("isLoading", false);
|
this.set("isLoading", false);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@computed("period")
|
@computed("period")
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export default Discourse.Route.extend({
|
export default Discourse.Route.extend({
|
||||||
setupController(controller) {
|
activate() {
|
||||||
controller.fetchDashboard();
|
this.controllerFor('admin-dashboard-next').fetchDashboard();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
{{#conditional-loading-spinner condition=isLoading}}
|
||||||
|
<div class="table-title">
|
||||||
|
<h3>{{title}}</h3>
|
||||||
|
|
||||||
|
{{#if help}}
|
||||||
|
<a href="{{helpPage}}">{{i18n help}}</a>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{{#each labels as |label|}}
|
||||||
|
<th>{{label}}</th>
|
||||||
|
{{/each}}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{{#each values as |value|}}
|
||||||
|
<tr>
|
||||||
|
{{#each value as |v|}}
|
||||||
|
<td>{{v}}</td>
|
||||||
|
{{/each}}
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{/conditional-loading-spinner}}
|
@ -1,72 +1,82 @@
|
|||||||
{{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>
|
{{period-chooser period=period action="changePeriod"}}
|
||||||
{{period-chooser period=period action="changePeriod"}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section-body">
|
|
||||||
<div class="charts">
|
|
||||||
{{dashboard-mini-chart
|
|
||||||
model=global_reports_signups
|
|
||||||
dataSourceName="signups"
|
|
||||||
startDate=startDate
|
|
||||||
endDate=endDate
|
|
||||||
help="admin.dashboard.charts.signups.help"}}
|
|
||||||
|
|
||||||
{{dashboard-mini-chart
|
|
||||||
model=global_reports_topics
|
|
||||||
dataSourceName="topics"
|
|
||||||
startDate=startDate
|
|
||||||
endDate=endDate
|
|
||||||
help="admin.dashboard.charts.topics.help"}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section-columns">
|
<div class="section-body">
|
||||||
<div class="section-column">
|
<div class="charts">
|
||||||
{{dashboard-mini-table model=user_reports_users_by_type isLoading=isLoading}}
|
{{dashboard-mini-chart
|
||||||
{{dashboard-mini-table model=user_reports_users_by_trust_level isLoading=isLoading}}
|
model=global_reports_signups
|
||||||
|
dataSourceName="signups"
|
||||||
|
startDate=startDate
|
||||||
|
endDate=endDate
|
||||||
|
help="admin.dashboard.charts.signups.help"}}
|
||||||
|
|
||||||
{{#conditional-loading-spinner isLoading=isLoading}}
|
{{dashboard-mini-chart
|
||||||
<div class="misc">
|
model=global_reports_topics
|
||||||
<div class="durability">
|
dataSourceName="topics"
|
||||||
{{#if currentUser.admin}}
|
startDate=startDate
|
||||||
<div class="backups">
|
endDate=endDate
|
||||||
<h3 class="durability-title"><a href="/admin/backups">{{i18n "admin.dashboard.backups"}}</a></h3>
|
help="admin.dashboard.charts.topics.help"}}
|
||||||
<p>
|
</div>
|
||||||
{{disk_space.backups_used}} ({{i18n "admin.dashboard.space_free" size=disk_space.backups_free}})
|
</div>
|
||||||
<br />
|
</div>
|
||||||
{{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<div class="uploads">
|
<div class="section-columns">
|
||||||
<h3 class="durability-title">{{i18n "admin.dashboard.uploads"}}</h3>
|
<div class="section-column">
|
||||||
|
{{dashboard-inline-table
|
||||||
|
model=user_reports_users_by_type
|
||||||
|
lastRefreshedAt=lastRefreshedAt
|
||||||
|
isLoading=isLoading}}
|
||||||
|
|
||||||
|
{{dashboard-inline-table
|
||||||
|
model=user_reports_users_by_trust_level
|
||||||
|
lastRefreshedAt=lastRefreshedAt
|
||||||
|
isLoading=isLoading}}
|
||||||
|
|
||||||
|
{{#conditional-loading-spinner isLoading=isLoading}}
|
||||||
|
<div class="misc">
|
||||||
|
<div class="durability">
|
||||||
|
{{#if currentUser.admin}}
|
||||||
|
<div class="backups">
|
||||||
|
<h3 class="durability-title"><a href="/admin/backups">{{i18n "admin.dashboard.backups"}}</a></h3>
|
||||||
<p>
|
<p>
|
||||||
{{disk_space.uploads_used}} ({{i18n "admin.dashboard.space_free" size=disk_space.uploads_free}})
|
{{disk_space.backups_used}} ({{i18n "admin.dashboard.space_free" size=disk_space.backups_free}})
|
||||||
|
<br />
|
||||||
|
{{{i18n "admin.dashboard.lastest_backup" date=backupTimestamp}}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<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}})
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<p class="last-dashboard-update">
|
|
||||||
{{i18n "admin.dashboard.last_updated"}} {{updatedTimestamp}}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<a rel="noopener" target="_blank" href="https://meta.discourse.org/t/discourse-2-0-0-beta6-release-notes/85241" class="btn">
|
|
||||||
{{i18n "admin.dashboard.whats_new_in_discourse"}}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
{{/conditional-loading-spinner}}
|
|
||||||
|
|
||||||
</div>
|
<hr />
|
||||||
|
|
||||||
<div class="section-column">
|
<p class="last-dashboard-update">
|
||||||
</div>
|
{{i18n "admin.dashboard.last_updated"}} {{updatedTimestamp}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a rel="noopener" target="_blank" href="https://meta.discourse.org/t/discourse-2-0-0-beta6-release-notes/85241" class="btn">
|
||||||
|
{{i18n "admin.dashboard.whats_new_in_discourse"}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{/conditional-loading-spinner}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="section-column">
|
||||||
|
{{dashboard-table-trending-search
|
||||||
|
model=global_reports_trending_search
|
||||||
|
dataSourceName="trending_search"
|
||||||
|
startDate=startDate
|
||||||
|
endDate=endDate}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@ -9,8 +9,15 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
.section-column {
|
.section-column {
|
||||||
flex: 1;
|
min-width: calc(50% - .5em);
|
||||||
flex-grow: 1;
|
}
|
||||||
|
|
||||||
|
.section-column:last-child {
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-column:first-child {
|
||||||
|
margin-right: .5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +40,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-mini-table {
|
.dashboard-table {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
|
||||||
&.is-loading {
|
&.is-loading {
|
||||||
@ -81,14 +88,9 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.dashboard-mini-chart {
|
.dashboard-mini-chart {
|
||||||
flex-grow: 1;
|
|
||||||
width: calc(100% * (1/3));
|
width: calc(100% * (1/3));
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
margin-right: 1em;
|
flex-grow: 1;
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-loading {
|
&.is-loading {
|
||||||
height: 150px;
|
height: 150px;
|
||||||
@ -104,7 +106,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: .5em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,6 +144,7 @@
|
|||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding: 0 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-trend {
|
.chart-trend {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
require 'disk_space'
|
require 'disk_space'
|
||||||
|
|
||||||
class Admin::DashboardNextController < Admin::AdminController
|
class Admin::DashboardNextController < Admin::AdminController
|
||||||
def index
|
def index
|
||||||
dashboard_data = AdminDashboardNextData.fetch_cached_stats || Jobs::DashboardNextStats.new.execute({})
|
dashboard_data = AdminDashboardNextData.fetch_stats
|
||||||
dashboard_data.merge!(version_check: DiscourseUpdates.check_version.as_json) if SiteSetting.version_checks?
|
|
||||||
dashboard_data[:disk_space] = DiskSpace.cached_stats
|
dashboard_data[:disk_space] = DiskSpace.cached_stats
|
||||||
render json: dashboard_data
|
render json: dashboard_data
|
||||||
end
|
end
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
require_dependency 'admin_dashboard_data'
|
|
||||||
require_dependency 'group'
|
|
||||||
require_dependency 'group_message'
|
|
||||||
|
|
||||||
module Jobs
|
|
||||||
class DashboardNextStats < Jobs::Scheduled
|
|
||||||
every 30.minutes
|
|
||||||
|
|
||||||
def execute(args)
|
|
||||||
problems_started_at = AdminDashboardNextData.problems_started_at
|
|
||||||
if problems_started_at && problems_started_at < 2.days.ago
|
|
||||||
# If there have been problems reported on the dashboard for a while,
|
|
||||||
# send a message to admins no more often than once per week.
|
|
||||||
GroupMessage.create(Group[:admins].name, :dashboard_problems, limit_once_per: 7.days.to_i)
|
|
||||||
end
|
|
||||||
|
|
||||||
AdminDashboardNextData.refresh_stats
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -4,6 +4,7 @@ class AdminDashboardNextData
|
|||||||
GLOBAL_REPORTS ||= [
|
GLOBAL_REPORTS ||= [
|
||||||
'signups',
|
'signups',
|
||||||
'topics',
|
'topics',
|
||||||
|
'trending_search'
|
||||||
]
|
]
|
||||||
|
|
||||||
USER_REPORTS ||= [
|
USER_REPORTS ||= [
|
||||||
|
@ -2,7 +2,7 @@ require_dependency 'topic_subtype'
|
|||||||
|
|
||||||
class Report
|
class Report
|
||||||
|
|
||||||
attr_accessor :type, :data, :total, :prev30Days, :start_date, :end_date, :category_id, :group_id
|
attr_accessor :type, :data, :total, :prev30Days, :start_date, :end_date, :category_id, :group_id, :labels
|
||||||
|
|
||||||
def self.default_days
|
def self.default_days
|
||||||
30
|
30
|
||||||
@ -26,7 +26,8 @@ class Report
|
|||||||
end_date: end_date,
|
end_date: end_date,
|
||||||
category_id: category_id,
|
category_id: category_id,
|
||||||
group_id: group_id,
|
group_id: group_id,
|
||||||
prev30Days: self.prev30Days
|
prev30Days: self.prev30Days,
|
||||||
|
labels: labels
|
||||||
}.tap do |json|
|
}.tap do |json|
|
||||||
if type == 'page_view_crawler_reqs'
|
if type == 'page_view_crawler_reqs'
|
||||||
json[:related_report] = Report.find('web_crawlers', start_date: start_date, end_date: end_date)&.as_json
|
json[:related_report] = Report.find('web_crawlers', start_date: start_date, end_date: end_date)&.as_json
|
||||||
@ -261,4 +262,27 @@ class Report
|
|||||||
silenced = User.real.silenced.count
|
silenced = User.real.silenced.count
|
||||||
report.data << { x: label.call("silenced"), y: silenced } if silenced > 0
|
report.data << { x: label.call("silenced"), y: silenced } if silenced > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.report_trending_search(report)
|
||||||
|
report.data = []
|
||||||
|
|
||||||
|
trends = SearchLog.select("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")
|
||||||
|
.where('created_at > ? AND created_at <= ?', report.start_date, report.end_date)
|
||||||
|
.group(:term)
|
||||||
|
.order('COUNT(DISTINCT ip_address) DESC, COUNT(*) DESC')
|
||||||
|
.limit(20).to_a
|
||||||
|
|
||||||
|
label = Proc.new { |key| I18n.t("reports.trending_search.labels.#{key}") }
|
||||||
|
report.labels = [:term, :searches, :unique].map {|key| label.call(key) }
|
||||||
|
|
||||||
|
trends.each do |trend|
|
||||||
|
report.data << [trend.term, trend.searches, trend.unique]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -878,6 +878,12 @@ en:
|
|||||||
moderator: Moderator
|
moderator: Moderator
|
||||||
suspended: Suspended
|
suspended: Suspended
|
||||||
silenced: Silenced
|
silenced: Silenced
|
||||||
|
trending_search:
|
||||||
|
title: Trending search
|
||||||
|
labels:
|
||||||
|
term: Term
|
||||||
|
searches: Searches
|
||||||
|
unique: Unique
|
||||||
emails:
|
emails:
|
||||||
title: "Emails Sent"
|
title: "Emails Sent"
|
||||||
xaxis: "Day"
|
xaxis: "Day"
|
||||||
|
@ -279,6 +279,37 @@ describe Report do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'trending search report' do
|
||||||
|
let(:report) { Report.find('trending_search') }
|
||||||
|
|
||||||
|
context "no searches" do
|
||||||
|
it "returns an empty report" do
|
||||||
|
expect(report.data).to be_blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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.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
|
||||||
|
|
||||||
|
it "returns a report with data" do
|
||||||
|
expect(report.data).to be_present
|
||||||
|
|
||||||
|
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[1][0]).to eq "php"
|
||||||
|
expect(report.data[1][1]).to eq 1
|
||||||
|
expect(report.data[1][2]).to eq 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'posts counts' do
|
describe 'posts counts' do
|
||||||
it "only counts regular posts" do
|
it "only counts regular posts" do
|
||||||
post = Fabricate(:post)
|
post = Fabricate(:post)
|
||||||
|
Reference in New Issue
Block a user