UI: improves dashboard table reports
- support for avatars - support for topic/post/user type in reports - improved totals row UI - minor css tweaks
This commit is contained in:
parent
6aee22b88f
commit
37252c1a5e
|
@ -3,10 +3,10 @@ import computed from "ember-addons/ember-computed-decorators";
|
|||
export default Ember.Component.extend({
|
||||
tagName: "th",
|
||||
classNames: ["admin-report-table-header"],
|
||||
classNameBindings: ["label.property", "isCurrentSort"],
|
||||
classNameBindings: ["label.mainProperty", "isCurrentSort"],
|
||||
attributeBindings: ["label.title:title"],
|
||||
|
||||
@computed("currentSortLabel.sort_property", "label.sort_property")
|
||||
@computed("currentSortLabel.sortProperty", "label.sortProperty")
|
||||
isCurrentSort(currentSortField, labelSortField) {
|
||||
return currentSortField === labelSortField;
|
||||
},
|
||||
|
|
|
@ -67,14 +67,14 @@ export default Ember.Component.extend({
|
|||
const computedLabel = label.compute(row);
|
||||
const value = computedLabel.value;
|
||||
|
||||
if (computedLabel.type === "link" || (value && !isNumeric(value))) {
|
||||
if (!computedLabel.countable || !value || !isNumeric(value)) {
|
||||
return undefined;
|
||||
} else {
|
||||
return sum + value;
|
||||
}
|
||||
};
|
||||
|
||||
totalsRow[label.property] = rows.reduce(reducer, 0);
|
||||
totalsRow[label.mainProperty] = rows.reduce(reducer, 0);
|
||||
});
|
||||
|
||||
return totalsRow;
|
||||
|
|
|
@ -2,7 +2,7 @@ import Category from "discourse/models/category";
|
|||
import { exportEntity } from "discourse/lib/export-csv";
|
||||
import { outputExportResult } from "discourse/lib/export-result";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import Report from "admin/models/report";
|
||||
import { SCHEMA_VERSION, default as Report } from "admin/models/report";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip";
|
||||
|
||||
|
@ -189,24 +189,20 @@ export default Ember.Component.extend({
|
|||
reportKey(dataSourceName, categoryId, groupId, startDate, endDate) {
|
||||
if (!dataSourceName || !startDate || !endDate) return null;
|
||||
|
||||
let reportKey = `reports:${dataSourceName}`;
|
||||
|
||||
if (categoryId && categoryId !== "all") {
|
||||
reportKey += `:${categoryId}`;
|
||||
} else {
|
||||
reportKey += `:`;
|
||||
}
|
||||
|
||||
reportKey += `:${startDate.replace(/-/g, "")}`;
|
||||
reportKey += `:${endDate.replace(/-/g, "")}`;
|
||||
|
||||
if (groupId && groupId !== "all") {
|
||||
reportKey += `:${groupId}`;
|
||||
} else {
|
||||
reportKey += `:`;
|
||||
}
|
||||
|
||||
reportKey += `:`;
|
||||
let reportKey = "reports:";
|
||||
reportKey += [
|
||||
dataSourceName,
|
||||
categoryId,
|
||||
startDate.replace(/-/g, ""),
|
||||
endDate.replace(/-/g, ""),
|
||||
groupId,
|
||||
"[:prev_period]",
|
||||
this.get("reportOptions.table.limit"),
|
||||
SCHEMA_VERSION
|
||||
]
|
||||
.filter(x => x)
|
||||
.map(x => x.toString())
|
||||
.join(":");
|
||||
|
||||
return reportKey;
|
||||
},
|
||||
|
|
|
@ -1,87 +1,24 @@
|
|||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import round from "discourse/lib/round";
|
||||
import { fillMissingDates, isNumeric } from "discourse/lib/utilities";
|
||||
import {
|
||||
fillMissingDates,
|
||||
isNumeric,
|
||||
formatUsername
|
||||
} from "discourse/lib/utilities";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { number, durationTiny } from "discourse/lib/formatter";
|
||||
import { renderAvatar } from "discourse/helpers/user-avatar";
|
||||
|
||||
// Change this line each time report format change
|
||||
// and you want to ensure cache is reset
|
||||
export const SCHEMA_VERSION = 1;
|
||||
|
||||
const Report = Discourse.Model.extend({
|
||||
average: false,
|
||||
percent: false,
|
||||
higher_is_better: true,
|
||||
|
||||
@computed("labels")
|
||||
computedLabels(labels) {
|
||||
return labels.map(label => {
|
||||
const type = label.type;
|
||||
const properties = label.properties;
|
||||
const property = properties[0];
|
||||
|
||||
return {
|
||||
title: label.title,
|
||||
sort_property: label.sort_property || property,
|
||||
property,
|
||||
compute: row => {
|
||||
let value = row[property];
|
||||
let escapedValue = escapeExpression(value);
|
||||
let tooltip;
|
||||
let base = { property, value, type };
|
||||
|
||||
if (value === null || typeof value === "undefined") {
|
||||
return _.assign(base, {
|
||||
value: null,
|
||||
formatedValue: "-",
|
||||
type: "undefined"
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "seconds") {
|
||||
return _.assign(base, {
|
||||
formatedValue: escapeExpression(durationTiny(value))
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "link") {
|
||||
return _.assign(base, {
|
||||
formatedValue: `<a href="${escapeExpression(
|
||||
row[properties[1]]
|
||||
)}">${escapedValue}</a>`
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "percent") {
|
||||
return _.assign(base, {
|
||||
formatedValue: `${escapedValue}%`
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "number" || isNumeric(value))
|
||||
return _.assign(base, {
|
||||
type: "number",
|
||||
formatedValue: number(value)
|
||||
});
|
||||
|
||||
if (type === "date") {
|
||||
const date = moment(value, "YYYY-MM-DD");
|
||||
if (date.isValid()) {
|
||||
return _.assign(base, {
|
||||
formatedValue: date.format("LL")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "text") tooltip = escapedValue;
|
||||
|
||||
return _.assign(base, {
|
||||
tooltip,
|
||||
type: type || "string",
|
||||
formatedValue: escapedValue
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
@computed("modes")
|
||||
onlyTable(modes) {
|
||||
return modes.length === 1 && modes[0] === "table";
|
||||
|
@ -312,6 +249,179 @@ const Report = Discourse.Model.extend({
|
|||
return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/);
|
||||
},
|
||||
|
||||
@computed("labels")
|
||||
computedLabels(labels) {
|
||||
return labels.map(label => {
|
||||
const type = label.type;
|
||||
|
||||
let mainProperty;
|
||||
if (label.property) mainProperty = label.property;
|
||||
else if (type === "user") mainProperty = label.properties["username"];
|
||||
else if (type === "topic") mainProperty = label.properties["title"];
|
||||
else if (type === "post")
|
||||
mainProperty = label.properties["truncated_raw"];
|
||||
else mainProperty = label.properties[0];
|
||||
|
||||
return {
|
||||
title: label.title,
|
||||
sortProperty: label.sort_property || mainProperty,
|
||||
mainProperty,
|
||||
compute: row => {
|
||||
const value = row[mainProperty];
|
||||
|
||||
if (type === "user") return this._userLabel(label.properties, row);
|
||||
if (type === "post") return this._postLabel(label.properties, row);
|
||||
if (type === "topic") return this._topicLabel(label.properties, row);
|
||||
if (type === "seconds")
|
||||
return this._secondsLabel(mainProperty, value);
|
||||
if (type === "link") return this._linkLabel(label.properties, row);
|
||||
if (type === "percent")
|
||||
return this._percentLabel(mainProperty, value);
|
||||
if (type === "number" || isNumeric(value)) {
|
||||
return this._numberLabel(mainProperty, value);
|
||||
}
|
||||
if (type === "date") {
|
||||
const date = moment(value, "YYYY-MM-DD");
|
||||
if (date.isValid())
|
||||
return this._dateLabel(mainProperty, value, date);
|
||||
}
|
||||
if (type === "text") return this._textLabel(mainProperty, value);
|
||||
if (!value) return this._undefinedLabel();
|
||||
|
||||
return {
|
||||
property: mainProperty,
|
||||
value,
|
||||
type: type || "string",
|
||||
formatedValue: escapeExpression(value)
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
_undefinedLabel() {
|
||||
return {
|
||||
value: null,
|
||||
formatedValue: "-",
|
||||
type: "undefined"
|
||||
};
|
||||
},
|
||||
|
||||
_userLabel(properties, row) {
|
||||
const username = row[properties.username];
|
||||
|
||||
if (!username) return this._undefinedLabel();
|
||||
|
||||
const user = Ember.Object.create({
|
||||
username,
|
||||
name: formatUsername(username),
|
||||
avatar_template: row[properties.avatar]
|
||||
});
|
||||
|
||||
const avatarImg = renderAvatar(user, {
|
||||
imageSize: "small",
|
||||
ignoreTitle: true
|
||||
});
|
||||
|
||||
const href = `/admin/users/${row[properties.id]}/${username}`;
|
||||
|
||||
return {
|
||||
type: "user",
|
||||
property: properties.username,
|
||||
value: username,
|
||||
formatedValue: `<a href='${href}'>${avatarImg}<span class='username'>${username}</span></a>`
|
||||
};
|
||||
},
|
||||
|
||||
_topicLabel(properties, row) {
|
||||
const topicTitle = row[properties.title];
|
||||
const topicId = row[properties.id];
|
||||
const href = `/t/-/${topicId}`;
|
||||
|
||||
return {
|
||||
type: "topic",
|
||||
property: properties.title,
|
||||
value: topicTitle,
|
||||
formatedValue: `<a href='${href}'>${topicTitle}</a>`
|
||||
};
|
||||
},
|
||||
|
||||
_postLabel(properties, row) {
|
||||
const postTitle = row[properties.truncated_raw];
|
||||
const postNumber = row[properties.number];
|
||||
const topicId = row[properties.topic_id];
|
||||
const href = `/t/-/${topicId}/${postNumber}`;
|
||||
|
||||
return {
|
||||
type: "post",
|
||||
property: properties.title,
|
||||
value: postTitle,
|
||||
formatedValue: `<a href='${href}'>${postTitle}</a>`
|
||||
};
|
||||
},
|
||||
|
||||
_secondsLabel(property, value) {
|
||||
return {
|
||||
value,
|
||||
property,
|
||||
countable: true,
|
||||
type: "seconds",
|
||||
formatedValue: durationTiny(value)
|
||||
};
|
||||
},
|
||||
|
||||
_percentLabel(property, value) {
|
||||
return {
|
||||
type: "percent",
|
||||
property,
|
||||
value,
|
||||
formatedValue: `${value}%`
|
||||
};
|
||||
},
|
||||
|
||||
_numberLabel(property, value) {
|
||||
return {
|
||||
type: "number",
|
||||
countable: true,
|
||||
property,
|
||||
value,
|
||||
formatedValue: number(value)
|
||||
};
|
||||
},
|
||||
|
||||
_dateLabel(property, value, date) {
|
||||
return {
|
||||
type: "date",
|
||||
property,
|
||||
value,
|
||||
formatedValue: date.format("LL")
|
||||
};
|
||||
},
|
||||
|
||||
_textLabel(property, value) {
|
||||
const escaped = escapeExpression(value);
|
||||
|
||||
return {
|
||||
type: "text",
|
||||
property,
|
||||
value,
|
||||
formatedValue: escaped
|
||||
};
|
||||
},
|
||||
|
||||
_linkLabel(properties, row) {
|
||||
const property = properties[0];
|
||||
const value = row[property];
|
||||
return {
|
||||
type: "link",
|
||||
property,
|
||||
value,
|
||||
formatedValue: `<a href="${escapeExpression(
|
||||
row[properties[1]]
|
||||
)}">${escapeExpression(value)}</a>`
|
||||
};
|
||||
},
|
||||
|
||||
_computeChange(valAtT1, valAtT2) {
|
||||
return ((valAtT2 - valAtT1) / valAtT1) * 100;
|
||||
},
|
||||
|
|
|
@ -21,33 +21,35 @@
|
|||
{{#each paginatedData as |data|}}
|
||||
{{admin-report-table-row data=data labels=model.computedLabels}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{#if showTotalForSample}}
|
||||
<small>{{i18n 'admin.dashboard.reports.totals_for_sample'}}</small>
|
||||
<table class="totals-sample-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
{{#if showTotalForSample}}
|
||||
<tr class="total-row">
|
||||
<td colspan="{{totalsForSample.length}}">
|
||||
{{i18n 'admin.dashboard.reports.totals_for_sample'}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="admin-report-table-row">
|
||||
{{#each totalsForSample as |total|}}
|
||||
<td>{{total.formatedValue}}</td>
|
||||
<td class="admin-report-table-row {{total.type}} {{total.property}}">
|
||||
{{total.formatedValue}}
|
||||
</td>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showTotal}}
|
||||
<small>{{i18n 'admin.dashboard.reports.total'}}</small>
|
||||
<table class="totals-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>-</td>
|
||||
<td>{{number model.total}}</td>
|
||||
{{#if showTotal}}
|
||||
<tr class="total-row">
|
||||
<td colspan="2">
|
||||
{{i18n 'admin.dashboard.reports.total'}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
<tr class="admin-report-table-row">
|
||||
<td class="date x">-</td>
|
||||
<td class="number y">{{number model.total}}</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pagination">
|
||||
{{#each pages as |pageState|}}
|
||||
|
|
|
@ -193,7 +193,7 @@
|
|||
}
|
||||
|
||||
.admin-report.post-edits {
|
||||
.admin-report-table {
|
||||
.report-table {
|
||||
table-layout: auto;
|
||||
|
||||
tbody tr td,
|
||||
|
@ -203,7 +203,7 @@
|
|||
|
||||
thead tr th.edit_reason,
|
||||
tbody tr td.edit_reason {
|
||||
width: 60%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
table {
|
||||
.report-table {
|
||||
table-layout: fixed;
|
||||
border: 1px solid $primary-low;
|
||||
margin-top: 0;
|
||||
|
@ -41,16 +41,28 @@
|
|||
tbody {
|
||||
border: none;
|
||||
|
||||
.total-row {
|
||||
td {
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
td {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
|
||||
&.user {
|
||||
text-align: left;
|
||||
|
||||
.username {
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,6 @@ class AdminDashboardNextData
|
|||
end
|
||||
|
||||
def self.stats_cache_key
|
||||
'dashboard-next-data'
|
||||
"dashboard-next-data-#{Report::SCHEMA_VERSION}"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,6 +20,6 @@ class AdminDashboardNextGeneralData < AdminDashboardNextData
|
|||
end
|
||||
|
||||
def self.stats_cache_key
|
||||
'general-dashboard-data'
|
||||
"general-dashboard-data-#{Report::SCHEMA_VERSION}"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ class AdminDashboardNextIndexData < AdminDashboardNextData
|
|||
end
|
||||
|
||||
def self.stats_cache_key
|
||||
'index-dashboard-data'
|
||||
"index-dashboard-data-#{Report::SCHEMA_VERSION}"
|
||||
end
|
||||
|
||||
# TODO: problems should be loaded from this model
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
require_dependency 'topic_subtype'
|
||||
|
||||
class Report
|
||||
# Change this line each time report format change
|
||||
# and you want to ensure cache is reset
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
attr_accessor :type, :data, :total, :prev30Days, :start_date,
|
||||
:end_date, :category_id, :group_id, :labels, :async,
|
||||
:prev_period, :facets, :limit, :processing, :average, :percent,
|
||||
|
@ -15,7 +19,7 @@ class Report
|
|||
def initialize(type)
|
||||
@type = type
|
||||
@start_date ||= Report.default_days.days.ago.beginning_of_day
|
||||
@end_date ||= Time.zone.now.end_of_day
|
||||
@end_date ||= Time.now.end_of_day
|
||||
@prev_end_date = @start_date
|
||||
@average = false
|
||||
@percent = false
|
||||
|
@ -36,8 +40,9 @@ class Report
|
|||
report.end_date.to_date.strftime("%Y%m%d"),
|
||||
report.group_id,
|
||||
report.facets,
|
||||
report.limit
|
||||
].map(&:to_s).join(':')
|
||||
report.limit,
|
||||
SCHEMA_VERSION,
|
||||
].compact.map(&:to_s).join(':')
|
||||
end
|
||||
|
||||
def self.clear_cache
|
||||
|
@ -72,33 +77,41 @@ class Report
|
|||
description = I18n.t("reports.#{type}.description", default: "")
|
||||
|
||||
{
|
||||
type: type,
|
||||
title: I18n.t("reports.#{type}.title"),
|
||||
xaxis: I18n.t("reports.#{type}.xaxis"),
|
||||
yaxis: I18n.t("reports.#{type}.yaxis"),
|
||||
description: description.presence ? description : nil,
|
||||
data: data,
|
||||
start_date: start_date&.iso8601,
|
||||
end_date: end_date&.iso8601,
|
||||
prev_data: self.prev_data,
|
||||
prev_start_date: prev_start_date&.iso8601,
|
||||
prev_end_date: prev_end_date&.iso8601,
|
||||
category_id: category_id,
|
||||
group_id: group_id,
|
||||
prev30Days: self.prev30Days,
|
||||
dates_filtering: self.dates_filtering,
|
||||
report_key: Report.cache_key(self),
|
||||
labels: labels || [
|
||||
{ type: :date, properties: [:x], title: I18n.t("reports.default.labels.day") },
|
||||
{ type: :number, properties: [:y], title: I18n.t("reports.default.labels.count") },
|
||||
],
|
||||
processing: self.processing,
|
||||
average: self.average,
|
||||
percent: self.percent,
|
||||
higher_is_better: self.higher_is_better,
|
||||
category_filtering: self.category_filtering,
|
||||
group_filtering: self.group_filtering,
|
||||
modes: self.modes
|
||||
type: type,
|
||||
title: I18n.t("reports.#{type}.title"),
|
||||
xaxis: I18n.t("reports.#{type}.xaxis"),
|
||||
yaxis: I18n.t("reports.#{type}.yaxis"),
|
||||
description: description.presence ? description : nil,
|
||||
data: data,
|
||||
start_date: start_date&.iso8601,
|
||||
end_date: end_date&.iso8601,
|
||||
prev_data: self.prev_data,
|
||||
prev_start_date: prev_start_date&.iso8601,
|
||||
prev_end_date: prev_end_date&.iso8601,
|
||||
category_id: category_id,
|
||||
group_id: group_id,
|
||||
prev30Days: self.prev30Days,
|
||||
dates_filtering: self.dates_filtering,
|
||||
report_key: Report.cache_key(self),
|
||||
labels: labels || [
|
||||
{
|
||||
type: :date,
|
||||
property: :x,
|
||||
title: I18n.t("reports.default.labels.day")
|
||||
},
|
||||
{
|
||||
type: :number,
|
||||
property: :y,
|
||||
title: I18n.t("reports.default.labels.count")
|
||||
},
|
||||
],
|
||||
processing: self.processing,
|
||||
average: self.average,
|
||||
percent: self.percent,
|
||||
higher_is_better: self.higher_is_better,
|
||||
category_filtering: self.category_filtering,
|
||||
group_filtering: self.group_filtering,
|
||||
modes: self.modes,
|
||||
}.tap do |json|
|
||||
json[:timeout] = self.timeout if self.timeout
|
||||
json[:total] = self.total if self.total
|
||||
|
@ -168,7 +181,7 @@ class Report
|
|||
ApplicationRequest.req_types.reject { |k, v| k =~ /mobile/ }.map { |k, v| v if k =~ /page_view/ }.compact
|
||||
].flatten)
|
||||
else
|
||||
ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter])
|
||||
ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter])
|
||||
end
|
||||
|
||||
if filter == :page_view_total
|
||||
|
@ -416,8 +429,14 @@ class Report
|
|||
report.dates_filtering = false
|
||||
|
||||
report.labels = [
|
||||
{ properties: [:key], title: I18n.t("reports.users_by_trust_level.labels.level") },
|
||||
{ properties: [:y], title: I18n.t("reports.default.labels.count") },
|
||||
{
|
||||
property: :key,
|
||||
title: I18n.t("reports.users_by_trust_level.labels.level")
|
||||
},
|
||||
{
|
||||
property: :y,
|
||||
title: I18n.t("reports.default.labels.count")
|
||||
}
|
||||
]
|
||||
|
||||
User.real.group('trust_level').count.sort.each do |level, count|
|
||||
|
@ -505,8 +524,15 @@ class Report
|
|||
|
||||
def self.report_web_crawlers(report)
|
||||
report.labels = [
|
||||
{ type: :string, properties: [:user_agent], title: I18n.t("reports.web_crawlers.labels.user_agent") },
|
||||
{ properties: [:count], title: I18n.t("reports.web_crawlers.labels.page_views") }
|
||||
{
|
||||
type: :string,
|
||||
property: :user_agent,
|
||||
title: I18n.t("reports.web_crawlers.labels.user_agent")
|
||||
},
|
||||
{
|
||||
property: :count,
|
||||
title: I18n.t("reports.web_crawlers.labels.page_views")
|
||||
}
|
||||
]
|
||||
report.modes = [:table]
|
||||
report.data = WebCrawlerRequest.where('date >= ? and date <= ?', report.start_date, report.end_date)
|
||||
|
@ -524,8 +550,14 @@ class Report
|
|||
report.dates_filtering = false
|
||||
|
||||
report.labels = [
|
||||
{ properties: [:x], title: I18n.t("reports.users_by_type.labels.type") },
|
||||
{ properties: [:y], title: I18n.t("reports.default.labels.count") },
|
||||
{
|
||||
property: :x,
|
||||
title: I18n.t("reports.users_by_type.labels.type")
|
||||
},
|
||||
{
|
||||
property: :y,
|
||||
title: I18n.t("reports.default.labels.count")
|
||||
}
|
||||
]
|
||||
|
||||
label = Proc.new { |x| I18n.t("reports.users_by_type.xaxis_labels.#{x}") }
|
||||
|
@ -548,8 +580,18 @@ class Report
|
|||
report.modes = [:table]
|
||||
|
||||
report.labels = [
|
||||
{ type: :link, properties: [:topic_title, :topic_url], title: I18n.t("reports.top_referred_topics.labels.topic") },
|
||||
{ properties: [:num_clicks], title: I18n.t("reports.top_referred_topics.labels.num_clicks") }
|
||||
{
|
||||
type: :topic,
|
||||
properties: {
|
||||
title: :topic_title,
|
||||
id: :topic_id
|
||||
},
|
||||
title: I18n.t("reports.top_referred_topics.labels.topic")
|
||||
},
|
||||
{
|
||||
property: :num_clicks,
|
||||
title: I18n.t("reports.top_referred_topics.labels.num_clicks")
|
||||
}
|
||||
]
|
||||
|
||||
options = { end_date: report.end_date, start_date: report.start_date, limit: report.limit || 8 }
|
||||
|
@ -564,9 +606,18 @@ class Report
|
|||
report.modes = [:table]
|
||||
|
||||
report.labels = [
|
||||
{ properties: [:domain], title: I18n.t("reports.top_traffic_sources.labels.domain") },
|
||||
{ properties: [:num_clicks], title: I18n.t("reports.top_traffic_sources.labels.num_clicks") },
|
||||
{ properties: [:num_topics], title: I18n.t("reports.top_traffic_sources.labels.num_topics") }
|
||||
{
|
||||
property: :domain,
|
||||
title: I18n.t("reports.top_traffic_sources.labels.domain")
|
||||
},
|
||||
{
|
||||
property: :num_clicks,
|
||||
title: I18n.t("reports.top_traffic_sources.labels.num_clicks")
|
||||
},
|
||||
{
|
||||
property: :num_topics,
|
||||
title: I18n.t("reports.top_traffic_sources.labels.num_topics")
|
||||
}
|
||||
]
|
||||
|
||||
options = { end_date: report.end_date, start_date: report.start_date, limit: report.limit || 8 }
|
||||
|
@ -579,9 +630,19 @@ class Report
|
|||
|
||||
def self.report_trending_search(report)
|
||||
report.labels = [
|
||||
{ properties: [:term], title: I18n.t("reports.trending_search.labels.term") },
|
||||
{ properties: [:unique_searches], title: I18n.t("reports.trending_search.labels.searches") },
|
||||
{ type: :percent, properties: [:ctr], title: I18n.t("reports.trending_search.labels.click_through") }
|
||||
{
|
||||
property: :term,
|
||||
title: I18n.t("reports.trending_search.labels.term")
|
||||
},
|
||||
{
|
||||
property: :unique_searches,
|
||||
title: I18n.t("reports.trending_search.labels.searches")
|
||||
},
|
||||
{
|
||||
type: :percent,
|
||||
property: :ctr,
|
||||
title: I18n.t("reports.trending_search.labels.click_through")
|
||||
}
|
||||
]
|
||||
|
||||
report.data = []
|
||||
|
@ -622,12 +683,36 @@ class Report
|
|||
|
||||
def self.report_moderators_activity(report)
|
||||
report.labels = [
|
||||
{ type: :link, properties: [:username, :user_url], title: I18n.t("reports.moderators_activity.labels.moderator") },
|
||||
{ properties: [:flag_count], title: I18n.t("reports.moderators_activity.labels.flag_count") },
|
||||
{ type: :seconds, properties: [:time_read], title: I18n.t("reports.moderators_activity.labels.time_read") },
|
||||
{ properties: [:topic_count], title: I18n.t("reports.moderators_activity.labels.topic_count") },
|
||||
{ properties: [:pm_count], title: I18n.t("reports.moderators_activity.labels.pm_count") },
|
||||
{ properties: [:post_count], title: I18n.t("reports.moderators_activity.labels.post_count") }
|
||||
{
|
||||
type: :user,
|
||||
properties: {
|
||||
username: :username,
|
||||
id: :user_id,
|
||||
avatar: :user_avatar_template,
|
||||
},
|
||||
title: I18n.t("reports.moderators_activity.labels.moderator"),
|
||||
},
|
||||
{
|
||||
property: :flag_count,
|
||||
title: I18n.t("reports.moderators_activity.labels.flag_count")
|
||||
},
|
||||
{
|
||||
type: :seconds,
|
||||
property: :time_read,
|
||||
title: I18n.t("reports.moderators_activity.labels.time_read")
|
||||
},
|
||||
{
|
||||
property: :topic_count,
|
||||
title: I18n.t("reports.moderators_activity.labels.topic_count")
|
||||
},
|
||||
{
|
||||
property: :pm_count,
|
||||
title: I18n.t("reports.moderators_activity.labels.pm_count")
|
||||
},
|
||||
{
|
||||
property: :post_count,
|
||||
title: I18n.t("reports.moderators_activity.labels.post_count")
|
||||
}
|
||||
]
|
||||
|
||||
report.modes = [:table]
|
||||
|
@ -638,8 +723,8 @@ class Report
|
|||
User.real.where(moderator: true).find_each do |u|
|
||||
mod_data[u.id] = {
|
||||
user_id: u.id,
|
||||
username: u.username,
|
||||
user_url: "/admin/users/#{u.id}/#{u.username}"
|
||||
username: u.username_lower,
|
||||
user_avatar_template: u.avatar_template,
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -766,11 +851,42 @@ class Report
|
|||
report.modes = [:table]
|
||||
|
||||
report.labels = [
|
||||
{ properties: [:action_type], title: I18n.t("reports.flags_status.labels.flag") },
|
||||
{ type: :link, properties: [:staff_username, :staff_url], title: I18n.t("reports.flags_status.labels.assigned") },
|
||||
{ type: :link, properties: [:poster_username, :poster_url], title: I18n.t("reports.flags_status.labels.poster") },
|
||||
{ type: :link, properties: [:flagger_username, :flagger_url], title: I18n.t("reports.flags_status.labels.flagger") },
|
||||
{ type: :seconds, properties: [:response_time], title: I18n.t("reports.flags_status.labels.time_to_resolution") }
|
||||
{
|
||||
property: :action_type,
|
||||
title: I18n.t("reports.flags_status.labels.flag")
|
||||
},
|
||||
{
|
||||
type: :user,
|
||||
properties: {
|
||||
username: :staff_username,
|
||||
id: :staff_id,
|
||||
avatar: :staff_avatar_template
|
||||
},
|
||||
title: I18n.t("reports.flags_status.labels.assigned")
|
||||
},
|
||||
{
|
||||
type: :user,
|
||||
properties: {
|
||||
username: :poster_username,
|
||||
id: :poster_id,
|
||||
avatar: :poster_avatar_template
|
||||
},
|
||||
title: I18n.t("reports.flags_status.labels.poster")
|
||||
},
|
||||
{
|
||||
type: :user,
|
||||
properties: {
|
||||
username: :flagger_username,
|
||||
id: :flagger_id,
|
||||
avatar: :flagger_avatar_template
|
||||
},
|
||||
title: I18n.t("reports.flags_status.labels.flagger")
|
||||
},
|
||||
{
|
||||
type: :seconds,
|
||||
property: :response_time,
|
||||
title: I18n.t("reports.flags_status.labels.time_to_resolution")
|
||||
}
|
||||
]
|
||||
|
||||
report.data = []
|
||||
|
@ -799,7 +915,8 @@ class Report
|
|||
poster_data AS (
|
||||
SELECT pa.id,
|
||||
p.user_id AS poster_id,
|
||||
u.username AS poster_username
|
||||
u.username_lower AS poster_username,
|
||||
u.uploaded_avatar_id AS poster_avatar_id
|
||||
FROM period_actions pa
|
||||
JOIN posts p
|
||||
ON p.id = pa.post_id
|
||||
|
@ -809,7 +926,8 @@ class Report
|
|||
flagger_data AS (
|
||||
SELECT pa.id,
|
||||
u.id AS flagger_id,
|
||||
u.username AS flagger_username
|
||||
u.username_lower AS flagger_username,
|
||||
u.uploaded_avatar_id AS flagger_avatar_id
|
||||
FROM period_actions pa
|
||||
JOIN users u
|
||||
ON u.id = pa.user_id
|
||||
|
@ -817,7 +935,8 @@ class Report
|
|||
staff_data AS (
|
||||
SELECT pa.id,
|
||||
u.id AS staff_id,
|
||||
u.username AS staff_username
|
||||
u.username_lower AS staff_username,
|
||||
u.uploaded_avatar_id AS staff_avatar_id
|
||||
FROM period_actions pa
|
||||
JOIN users u
|
||||
ON u.id = COALESCE(pa.agreed_by_id, pa.disagreed_by_id, pa.deferred_by_id)
|
||||
|
@ -825,10 +944,13 @@ class Report
|
|||
SELECT
|
||||
sd.staff_username,
|
||||
sd.staff_id,
|
||||
sd.staff_avatar_id,
|
||||
pd.poster_username,
|
||||
pd.poster_id,
|
||||
pd.poster_avatar_id,
|
||||
fd.flagger_username,
|
||||
fd.flagger_id,
|
||||
fd.flagger_avatar_id,
|
||||
pa.post_action_type_id,
|
||||
pa.created_at,
|
||||
pa.agreed_at,
|
||||
|
@ -850,17 +972,23 @@ class Report
|
|||
DB.query(sql).each do |row|
|
||||
data = {}
|
||||
data[:action_type] = flag_types.key(row.post_action_type_id).to_s
|
||||
data[:staff_username] = row.staff_username
|
||||
data[:staff_id] = row.staff_id
|
||||
if row.staff_username && row.staff_id
|
||||
data[:staff_url] = "/admin/users/#{row.staff_id}/#{row.staff_username}"
|
||||
|
||||
if row.staff_id
|
||||
data[:staff_username] = row.staff_username
|
||||
data[:staff_id] = row.staff_id
|
||||
data[:staff_avatar_template] = User.avatar_template(row.staff_username, row.staff_avatar_id)
|
||||
end
|
||||
data[:poster_username] = row.poster_username
|
||||
data[:poster_id] = row.poster_id
|
||||
data[:poster_url] = "/admin/users/#{row.poster_id}/#{row.poster_username}"
|
||||
|
||||
if row.poster_id
|
||||
data[:poster_username] = row.poster_username
|
||||
data[:poster_id] = row.poster_id
|
||||
data[:poster_avatar_template] = User.avatar_template(row.poster_username, row.poster_avatar_id)
|
||||
end
|
||||
|
||||
data[:flagger_id] = row.flagger_id
|
||||
data[:flagger_username] = row.flagger_username
|
||||
data[:flagger_url] = "/admin/users/#{row.flagger_id}/#{row.flagger_username}"
|
||||
data[:flagger_avatar_template] = User.avatar_template(row.flagger_username, row.flagger_avatar_id)
|
||||
|
||||
if row.agreed_by_id
|
||||
data[:resolution] = I18n.t("reports.flags_status.values.agreed")
|
||||
elsif row.disagreed_by_id
|
||||
|
@ -879,10 +1007,38 @@ class Report
|
|||
report.modes = [:table]
|
||||
|
||||
report.labels = [
|
||||
{ type: :link, properties: [:post_id, :post_url], title: I18n.t("reports.post_edits.labels.post") },
|
||||
{ type: :link, properties: [:editor_username, :editor_url], title: I18n.t("reports.post_edits.labels.editor") },
|
||||
{ type: :link, properties: [:author_username, :author_url], title: I18n.t("reports.post_edits.labels.author") },
|
||||
{ type: :text, properties: [:edit_reason], title: I18n.t("reports.post_edits.labels.edit_reason") }
|
||||
{
|
||||
type: :post,
|
||||
properties: {
|
||||
topic_id: :topic_id,
|
||||
number: :post_number,
|
||||
truncated_raw: :post_raw
|
||||
},
|
||||
title: I18n.t("reports.post_edits.labels.post")
|
||||
},
|
||||
{
|
||||
type: :user,
|
||||
properties: {
|
||||
username: :editor_username,
|
||||
id: :editor_id,
|
||||
avatar: :editor_avatar_template,
|
||||
},
|
||||
title: I18n.t("reports.post_edits.labels.editor")
|
||||
},
|
||||
{
|
||||
type: :user,
|
||||
properties: {
|
||||
username: :author_username,
|
||||
id: :author_id,
|
||||
avatar: :author_avatar_template,
|
||||
},
|
||||
title: I18n.t("reports.post_edits.labels.author")
|
||||
},
|
||||
{
|
||||
type: :text,
|
||||
property: :edit_reason,
|
||||
title: I18n.t("reports.post_edits.labels.edit_reason")
|
||||
},
|
||||
]
|
||||
|
||||
report.data = []
|
||||
|
@ -893,7 +1049,8 @@ class Report
|
|||
pr.number AS revision_version,
|
||||
pr.created_at,
|
||||
pr.post_id,
|
||||
u.username AS editor_username
|
||||
u.username AS editor_username,
|
||||
u.uploaded_avatar_id as editor_avatar_id
|
||||
FROM post_revisions pr
|
||||
JOIN users u
|
||||
ON u.id = pr.user_id
|
||||
|
@ -905,11 +1062,14 @@ class Report
|
|||
)
|
||||
SELECT pr.editor_id,
|
||||
pr.editor_username,
|
||||
pr.editor_avatar_id,
|
||||
p.user_id AS author_id,
|
||||
u.username AS author_username,
|
||||
u.uploaded_avatar_id AS author_avatar_id,
|
||||
pr.revision_version,
|
||||
p.version AS post_version,
|
||||
pr.post_id,
|
||||
left(p.raw, 40) AS post_raw,
|
||||
p.topic_id,
|
||||
p.post_number,
|
||||
p.edit_reason,
|
||||
|
@ -925,14 +1085,15 @@ class Report
|
|||
revision = {}
|
||||
revision[:editor_id] = r.editor_id
|
||||
revision[:editor_username] = r.editor_username
|
||||
revision[:editor_url] = "/admin/users/#{r.editor_id}/#{r.editor_username}"
|
||||
revision[:editor_avatar_template] = User.avatar_template(r.editor_username, r.editor_avatar_id)
|
||||
revision[:author_id] = r.author_id
|
||||
revision[:author_username] = r.author_username
|
||||
revision[:author_url] = "/admin/users/#{r.author_id}/#{r.author_username}"
|
||||
revision[:author_avatar_template] = User.avatar_template(r.author_username, r.author_avatar_id)
|
||||
revision[:edit_reason] = r.revision_version == r.post_version ? r.edit_reason : nil
|
||||
revision[:created_at] = r.created_at
|
||||
revision[:post_id] = r.post_id
|
||||
revision[:post_url] = "/t/-/#{r.topic_id}/#{r.post_number}"
|
||||
revision[:post_raw] = r.post_raw
|
||||
revision[:topic_id] = r.topic_id
|
||||
revision[:post_number] = r.post_number
|
||||
|
||||
report.data << revision
|
||||
end
|
||||
|
|
|
@ -60,7 +60,7 @@ componentTest("default", {
|
|||
"it has rows"
|
||||
);
|
||||
|
||||
assert.ok(exists(".totals-sample-table"), "it has totals");
|
||||
assert.ok(exists(".total-row"), "it has totals");
|
||||
|
||||
await click(".admin-report-table-header.y .sort-button");
|
||||
assert.equal(
|
||||
|
|
|
@ -396,22 +396,48 @@ QUnit.test("computed labels", assert => {
|
|||
const data = [
|
||||
{
|
||||
username: "joffrey",
|
||||
user_url: "/admin/users/1/joffrey",
|
||||
user_id: 1,
|
||||
user_avatar: "/",
|
||||
flag_count: 1876,
|
||||
time_read: 287362,
|
||||
note: "This is a long note"
|
||||
note: "This is a long note",
|
||||
topic_id: 2,
|
||||
topic_title: "Test topic",
|
||||
post_number: 3,
|
||||
post_raw: "This is the beginning of"
|
||||
}
|
||||
];
|
||||
|
||||
const labels = [
|
||||
{
|
||||
type: "link",
|
||||
properties: ["username", "user_url"],
|
||||
title: "Username"
|
||||
type: "user",
|
||||
properties: {
|
||||
username: "username",
|
||||
id: "user_id",
|
||||
avatar: "user_avatar"
|
||||
},
|
||||
title: "Moderator"
|
||||
},
|
||||
{ properties: ["flag_count"], title: "Flag count" },
|
||||
{ type: "seconds", properties: ["time_read"], title: "Time read" },
|
||||
{ type: "text", properties: ["note"], title: "Note" }
|
||||
{ property: "flag_count", title: "Flag count" },
|
||||
{ type: "seconds", property: "time_read", title: "Time read" },
|
||||
{ type: "text", property: "note", title: "Note" },
|
||||
{
|
||||
type: "topic",
|
||||
properties: {
|
||||
title: "topic_title",
|
||||
id: "topic_id"
|
||||
},
|
||||
title: "Topic"
|
||||
},
|
||||
{
|
||||
type: "post",
|
||||
properties: {
|
||||
topic_id: "topic_id",
|
||||
number: "post_number",
|
||||
truncated_raw: "post_raw"
|
||||
},
|
||||
title: "Post"
|
||||
}
|
||||
];
|
||||
|
||||
const report = Report.create({
|
||||
|
@ -424,20 +450,20 @@ QUnit.test("computed labels", assert => {
|
|||
const computedLabels = report.get("computedLabels");
|
||||
|
||||
const usernameLabel = computedLabels[0];
|
||||
assert.equal(usernameLabel.property, "username");
|
||||
assert.equal(usernameLabel.sort_property, "username");
|
||||
assert.equal(usernameLabel.title, "Username");
|
||||
assert.equal(usernameLabel.mainProperty, "username");
|
||||
assert.equal(usernameLabel.sortProperty, "username");
|
||||
assert.equal(usernameLabel.title, "Moderator");
|
||||
const computedUsernameLabel = usernameLabel.compute(row);
|
||||
assert.equal(
|
||||
computedUsernameLabel.formatedValue,
|
||||
'<a href="/admin/users/1/joffrey">joffrey</a>'
|
||||
"<a href='/admin/users/1/joffrey'><img alt='' width='25' height='25' src='/' class='avatar' title='joffrey'><span class='username'>joffrey</span></a>"
|
||||
);
|
||||
assert.equal(computedUsernameLabel.type, "link");
|
||||
assert.equal(computedUsernameLabel.type, "user");
|
||||
assert.equal(computedUsernameLabel.value, "joffrey");
|
||||
|
||||
const flagCountLabel = computedLabels[1];
|
||||
assert.equal(flagCountLabel.property, "flag_count");
|
||||
assert.equal(flagCountLabel.sort_property, "flag_count");
|
||||
assert.equal(flagCountLabel.mainProperty, "flag_count");
|
||||
assert.equal(flagCountLabel.sortProperty, "flag_count");
|
||||
assert.equal(flagCountLabel.title, "Flag count");
|
||||
const computedFlagCountLabel = flagCountLabel.compute(row);
|
||||
assert.equal(computedFlagCountLabel.formatedValue, "1.9k");
|
||||
|
@ -445,8 +471,8 @@ QUnit.test("computed labels", assert => {
|
|||
assert.equal(computedFlagCountLabel.value, 1876);
|
||||
|
||||
const timeReadLabel = computedLabels[2];
|
||||
assert.equal(timeReadLabel.property, "time_read");
|
||||
assert.equal(timeReadLabel.sort_property, "time_read");
|
||||
assert.equal(timeReadLabel.mainProperty, "time_read");
|
||||
assert.equal(timeReadLabel.sortProperty, "time_read");
|
||||
assert.equal(timeReadLabel.title, "Time read");
|
||||
const computedTimeReadLabel = timeReadLabel.compute(row);
|
||||
assert.equal(computedTimeReadLabel.formatedValue, "3d");
|
||||
|
@ -454,11 +480,35 @@ QUnit.test("computed labels", assert => {
|
|||
assert.equal(computedTimeReadLabel.value, 287362);
|
||||
|
||||
const noteLabel = computedLabels[3];
|
||||
assert.equal(noteLabel.property, "note");
|
||||
assert.equal(noteLabel.sort_property, "note");
|
||||
assert.equal(noteLabel.mainProperty, "note");
|
||||
assert.equal(noteLabel.sortProperty, "note");
|
||||
assert.equal(noteLabel.title, "Note");
|
||||
const computedNoteLabel = noteLabel.compute(row);
|
||||
assert.equal(computedNoteLabel.formatedValue, "This is a long note");
|
||||
assert.equal(computedNoteLabel.type, "text");
|
||||
assert.equal(computedNoteLabel.value, "This is a long note");
|
||||
|
||||
const topicLabel = computedLabels[4];
|
||||
assert.equal(topicLabel.mainProperty, "topic_title");
|
||||
assert.equal(topicLabel.sortProperty, "topic_title");
|
||||
assert.equal(topicLabel.title, "Topic");
|
||||
const computedTopicLabel = topicLabel.compute(row);
|
||||
assert.equal(
|
||||
computedTopicLabel.formatedValue,
|
||||
"<a href='/t/-/2'>Test topic</a>"
|
||||
);
|
||||
assert.equal(computedTopicLabel.type, "topic");
|
||||
assert.equal(computedTopicLabel.value, "Test topic");
|
||||
|
||||
const postLabel = computedLabels[5];
|
||||
assert.equal(postLabel.mainProperty, "post_raw");
|
||||
assert.equal(postLabel.sortProperty, "post_raw");
|
||||
assert.equal(postLabel.title, "Post");
|
||||
const computedPostLabel = postLabel.compute(row);
|
||||
assert.equal(
|
||||
computedPostLabel.formatedValue,
|
||||
"<a href='/t/-/2/3'>This is the beginning of</a>"
|
||||
);
|
||||
assert.equal(computedPostLabel.type, "post");
|
||||
assert.equal(computedPostLabel.value, "This is the beginning of");
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue