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:
Joffrey JAFFEUX 2018-07-31 17:35:13 -04:00 committed by GitHub
parent 6aee22b88f
commit 37252c1a5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 555 additions and 224 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,6 @@ class AdminDashboardNextData
end
def self.stats_cache_key
'dashboard-next-data'
"dashboard-next-data-#{Report::SCHEMA_VERSION}"
end
end

View File

@ -20,6 +20,6 @@ class AdminDashboardNextGeneralData < AdminDashboardNextData
end
def self.stats_cache_key
'general-dashboard-data'
"general-dashboard-data-#{Report::SCHEMA_VERSION}"
end
end

View File

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

View File

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

View File

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

View File

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