FIX: improves number/percent support in reports

This commit is contained in:
Joffrey JAFFEUX
2018-08-01 18:40:59 -04:00
committed by GitHub
parent 4a872823e7
commit 9073e11943
13 changed files with 127 additions and 104 deletions

View File

@ -0,0 +1,18 @@
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
tagName: "td",
classNames: ["admin-report-table-cell"],
classNameBindings: ["type", "property"],
options: null,
@computed("label", "data", "options")
computedLabel(label, data, options) {
return label.compute(data, options || {});
},
type: Ember.computed.alias("label.type"),
property: Ember.computed.alias("label.mainProperty"),
formatedValue: Ember.computed.alias("computedLabel.formatedValue"),
value: Ember.computed.alias("computedLabel.value")
});

View File

@ -3,7 +3,7 @@ import computed from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({ export default Ember.Component.extend({
tagName: "th", tagName: "th",
classNames: ["admin-report-table-header"], classNames: ["admin-report-table-header"],
classNameBindings: ["label.mainProperty", "isCurrentSort"], classNameBindings: ["label.mainProperty", "label.type", "isCurrentSort"],
attributeBindings: ["label.title:title"], attributeBindings: ["label.title:title"],
@computed("currentSortLabel.sortProperty", "label.sortProperty") @computed("currentSortLabel.sortProperty", "label.sortProperty")

View File

@ -1,13 +1,5 @@
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({ export default Ember.Component.extend({
tagName: "tr", tagName: "tr",
classNames: ["admin-report-table-row"], classNames: ["admin-report-table-row"],
options: null
@computed("data", "labels")
cells(row, labels) {
return labels.map(label => {
return label.compute(row);
});
}
}); });

View File

@ -1,6 +1,5 @@
import computed from "ember-addons/ember-computed-decorators"; import computed from "ember-addons/ember-computed-decorators";
import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip"; import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip";
import { isNumeric } from "discourse/lib/utilities";
const PAGES_LIMIT = 8; const PAGES_LIMIT = 8;
@ -67,14 +66,16 @@ export default Ember.Component.extend({
const computedLabel = label.compute(row); const computedLabel = label.compute(row);
const value = computedLabel.value; const value = computedLabel.value;
if (!computedLabel.countable || !value || !isNumeric(value)) { if (!["seconds", "number", "percent"].includes(label.type)) {
return undefined; return;
} else { } else {
return sum + value; return sum + Math.round(value || 0);
} }
}; };
totalsRow[label.mainProperty] = rows.reduce(reducer, 0); const total = rows.reduce(reducer, 0);
totalsRow[label.mainProperty] =
label.type === "percent" ? Math.round(total / rows.length) : total;
}); });
return totalsRow; return totalsRow;

View File

@ -9,7 +9,8 @@ import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip";
const TABLE_OPTIONS = { const TABLE_OPTIONS = {
perPage: 8, perPage: 8,
total: true, total: true,
limit: 20 limit: 20,
formatNumbers: true
}; };
const CHART_OPTIONS = {}; const CHART_OPTIONS = {};
@ -347,12 +348,12 @@ export default Ember.Component.extend({
if (mode === "table") { if (mode === "table") {
const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS)); const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS));
return Ember.Object.create( return Ember.Object.create(
_.assign(tableOptions, this.get("reportOptions.table") || {}) Object.assign(tableOptions, this.get("reportOptions.table") || {})
); );
} else { } else {
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS)); const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
return Ember.Object.create( return Ember.Object.create(
_.assign(chartOptions, this.get("reportOptions.chart") || {}) Object.assign(chartOptions, this.get("reportOptions.chart") || {})
); );
} }
}, },

View File

@ -5,7 +5,7 @@ export default Ember.Controller.extend({
@computed("model.type") @computed("model.type")
reportOptions(type) { reportOptions(type) {
let options = { table: { perPage: 50, limit: 50 } }; let options = { table: { perPage: 50, limit: 50, formatNumbers: false } };
if (type === "top_referred_topics") { if (type === "top_referred_topics") {
options.table.limit = 10; options.table.limit = 10;

View File

@ -1,11 +1,7 @@
import { escapeExpression } from "discourse/lib/utilities"; import { escapeExpression } from "discourse/lib/utilities";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import round from "discourse/lib/round"; import round from "discourse/lib/round";
import { import { fillMissingDates, formatUsername } from "discourse/lib/utilities";
fillMissingDates,
isNumeric,
formatUsername
} from "discourse/lib/utilities";
import computed from "ember-addons/ember-computed-decorators"; import computed from "ember-addons/ember-computed-decorators";
import { number, durationTiny } from "discourse/lib/formatter"; import { number, durationTiny } from "discourse/lib/formatter";
import { renderAvatar } from "discourse/helpers/user-avatar"; import { renderAvatar } from "discourse/helpers/user-avatar";
@ -252,7 +248,7 @@ const Report = Discourse.Model.extend({
@computed("labels") @computed("labels")
computedLabels(labels) { computedLabels(labels) {
return labels.map(label => { return labels.map(label => {
const type = label.type; const type = label.type || "string";
let mainProperty; let mainProperty;
if (label.property) mainProperty = label.property; if (label.property) mainProperty = label.property;
@ -266,51 +262,41 @@ const Report = Discourse.Model.extend({
title: label.title, title: label.title,
sortProperty: label.sort_property || mainProperty, sortProperty: label.sort_property || mainProperty,
mainProperty, mainProperty,
compute: row => { type,
compute: (row, opts = {}) => {
const value = row[mainProperty]; const value = row[mainProperty];
if (type === "user") return this._userLabel(label.properties, row); if (type === "user") return this._userLabel(label.properties, row);
if (type === "post") return this._postLabel(label.properties, row); if (type === "post") return this._postLabel(label.properties, row);
if (type === "topic") return this._topicLabel(label.properties, row); if (type === "topic") return this._topicLabel(label.properties, row);
if (type === "seconds") if (type === "seconds") return this._secondsLabel(value);
return this._secondsLabel(mainProperty, value);
if (type === "link") return this._linkLabel(label.properties, row); if (type === "link") return this._linkLabel(label.properties, row);
if (type === "percent") if (type === "percent") return this._percentLabel(value);
return this._percentLabel(mainProperty, value); if (type === "number") {
if (type === "number" || isNumeric(value)) { return this._numberLabel(value, opts);
return this._numberLabel(mainProperty, value);
} }
if (type === "date") { if (type === "date") {
const date = moment(value, "YYYY-MM-DD"); const date = moment(value, "YYYY-MM-DD");
if (date.isValid()) if (date.isValid()) return this._dateLabel(value, date);
return this._dateLabel(mainProperty, value, date);
} }
if (type === "text") return this._textLabel(mainProperty, value); if (type === "text") return this._textLabel(value);
if (!value) return this._undefinedLabel();
return { return {
property: mainProperty,
value, value,
type: type || "string", type,
formatedValue: escapeExpression(value) property: mainProperty,
formatedValue: value ? escapeExpression(value) : "-"
}; };
} }
}; };
}); });
}, },
_undefinedLabel() {
return {
value: null,
formatedValue: "-",
type: "undefined"
};
},
_userLabel(properties, row) { _userLabel(properties, row) {
const username = row[properties.username]; const username = row[properties.username];
if (!username) return this._undefinedLabel(); const formatedValue = () => {
const userId = row[properties.id];
const user = Ember.Object.create({ const user = Ember.Object.create({
username, username,
@ -318,18 +304,21 @@ const Report = Discourse.Model.extend({
avatar_template: row[properties.avatar] avatar_template: row[properties.avatar]
}); });
const href = `/admin/users/${userId}/${username}`;
const avatarImg = renderAvatar(user, { const avatarImg = renderAvatar(user, {
imageSize: "tiny", imageSize: "tiny",
ignoreTitle: true ignoreTitle: true
}); });
const href = `/admin/users/${row[properties.id]}/${username}`; return `<a href='${href}'>${avatarImg}<span class='username'>${
user.name
}</span></a>`;
};
return { return {
type: "user",
property: properties.username,
value: username, value: username,
formatedValue: `<a href='${href}'>${avatarImg}<span class='username'>${username}</span></a>` formatedValue: username ? formatedValue(username) : "-"
}; };
}, },
@ -339,8 +328,6 @@ const Report = Discourse.Model.extend({
const href = `/t/-/${topicId}`; const href = `/t/-/${topicId}`;
return { return {
type: "topic",
property: properties.title,
value: topicTitle, value: topicTitle,
formatedValue: `<a href='${href}'>${topicTitle}</a>` formatedValue: `<a href='${href}'>${topicTitle}</a>`
}; };
@ -353,72 +340,67 @@ const Report = Discourse.Model.extend({
const href = `/t/-/${topicId}/${postNumber}`; const href = `/t/-/${topicId}/${postNumber}`;
return { return {
type: "post",
property: properties.title, property: properties.title,
value: postTitle, value: postTitle,
formatedValue: `<a href='${href}'>${postTitle}</a>` formatedValue: `<a href='${href}'>${postTitle}</a>`
}; };
}, },
_secondsLabel(property, value) { _secondsLabel(value) {
return { return {
value, value,
property,
countable: true,
type: "seconds",
formatedValue: durationTiny(value) formatedValue: durationTiny(value)
}; };
}, },
_percentLabel(property, value) { _percentLabel(value) {
return { return {
type: "percent",
property,
value, value,
formatedValue: `${value}%` formatedValue: value ? `${value}%` : "-"
}; };
}, },
_numberLabel(property, value) { _numberLabel(value, options = {}) {
const formatNumbers = Ember.isEmpty(options.formatNumbers)
? true
: options.formatNumbers;
const formatedValue = () => (formatNumbers ? number(value) : value);
return { return {
type: "number",
countable: true,
property,
value, value,
formatedValue: number(value) formatedValue: value ? formatedValue() : "-"
}; };
}, },
_dateLabel(property, value, date) { _dateLabel(value, date) {
return { return {
type: "date",
property,
value, value,
formatedValue: date.format("LL") formatedValue: value ? date.format("LL") : "-"
}; };
}, },
_textLabel(property, value) { _textLabel(value) {
const escaped = escapeExpression(value); const escaped = escapeExpression(value);
return { return {
type: "text",
property,
value, value,
formatedValue: escaped formatedValue: value ? escaped : "-"
}; };
}, },
_linkLabel(properties, row) { _linkLabel(properties, row) {
const property = properties[0]; const property = properties[0];
const value = row[property]; const value = row[property];
const formatedValue = (href, anchor) => {
return `<a href="${escapeExpression(href)}">${escapeExpression(
anchor
)}</a>`;
};
return { return {
type: "link",
property,
value, value,
formatedValue: `<a href="${escapeExpression( formatedValue: value ? formatedValue(value, row[properties[1]]) : "-"
row[properties[1]]
)}">${escapeExpression(value)}</a>`
}; };
}, },

View File

@ -0,0 +1 @@
{{{formatedValue}}}

View File

@ -1,5 +1,3 @@
{{#each cells as |cell|}} {{#each labels as |label|}}
<td class="{{cell.type}} {{cell.property}}" title="{{cell.tooltip}}"> {{admin-report-table-cell label=label data=data options=options}}
{{{cell.formatedValue}}}
</td>
{{/each}} {{/each}}

View File

@ -19,7 +19,7 @@
</thead> </thead>
<tbody> <tbody>
{{#each paginatedData as |data|}} {{#each paginatedData as |data|}}
{{admin-report-table-row data=data labels=model.computedLabels}} {{admin-report-table-row data=data labels=model.computedLabels options=options}}
{{/each}} {{/each}}
{{#if showTotalForSample}} {{#if showTotalForSample}}
@ -30,7 +30,7 @@
</tr> </tr>
<tr class="admin-report-table-row"> <tr class="admin-report-table-row">
{{#each totalsForSample as |total|}} {{#each totalsForSample as |total|}}
<td class="admin-report-table-row {{total.type}} {{total.property}}"> <td class="{{total.type}} {{total.property}}">
{{total.formatedValue}} {{total.formatedValue}}
</td> </td>
{{/each}} {{/each}}

View File

@ -296,6 +296,19 @@ class Report
end end
def self.report_dau_by_mau(report) def self.report_dau_by_mau(report)
report.labels = [
{
type: :date,
property: :x,
title: I18n.t("reports.default.labels.day")
},
{
type: :percent,
property: :y,
title: I18n.t("reports.default.labels.percent")
},
]
report.average = true report.average = true
report.percent = true report.percent = true
@ -441,6 +454,7 @@ class Report
}, },
{ {
property: :y, property: :y,
type: :number,
title: I18n.t("reports.default.labels.count") title: I18n.t("reports.default.labels.count")
} }
] ]
@ -537,6 +551,7 @@ class Report
}, },
{ {
property: :count, property: :count,
type: :number,
title: I18n.t("reports.web_crawlers.labels.page_views") title: I18n.t("reports.web_crawlers.labels.page_views")
} }
] ]
@ -562,6 +577,7 @@ class Report
}, },
{ {
property: :y, property: :y,
type: :number,
title: I18n.t("reports.default.labels.count") title: I18n.t("reports.default.labels.count")
} }
] ]
@ -596,6 +612,7 @@ class Report
}, },
{ {
property: :num_clicks, property: :num_clicks,
type: :number,
title: I18n.t("reports.top_referred_topics.labels.num_clicks") title: I18n.t("reports.top_referred_topics.labels.num_clicks")
} }
] ]
@ -616,10 +633,12 @@ class Report
}, },
{ {
property: :num_clicks, property: :num_clicks,
type: :number,
title: I18n.t("reports.top_traffic_sources.labels.num_clicks") title: I18n.t("reports.top_traffic_sources.labels.num_clicks")
}, },
{ {
property: :num_topics, property: :num_topics,
type: :number,
title: I18n.t("reports.top_traffic_sources.labels.num_topics") title: I18n.t("reports.top_traffic_sources.labels.num_topics")
} }
] ]
@ -638,6 +657,7 @@ class Report
}, },
{ {
property: :unique_searches, property: :unique_searches,
type: :number,
title: I18n.t("reports.trending_search.labels.searches") title: I18n.t("reports.trending_search.labels.searches")
}, },
{ {
@ -696,6 +716,7 @@ class Report
}, },
{ {
property: :flag_count, property: :flag_count,
type: :number,
title: I18n.t("reports.moderators_activity.labels.flag_count") title: I18n.t("reports.moderators_activity.labels.flag_count")
}, },
{ {
@ -705,18 +726,22 @@ class Report
}, },
{ {
property: :topic_count, property: :topic_count,
type: :number,
title: I18n.t("reports.moderators_activity.labels.topic_count") title: I18n.t("reports.moderators_activity.labels.topic_count")
}, },
{ {
property: :pm_count, property: :pm_count,
type: :number,
title: I18n.t("reports.moderators_activity.labels.pm_count") title: I18n.t("reports.moderators_activity.labels.pm_count")
}, },
{ {
property: :post_count, property: :post_count,
type: :number,
title: I18n.t("reports.moderators_activity.labels.post_count") title: I18n.t("reports.moderators_activity.labels.post_count")
}, },
{ {
property: :revision_count, property: :revision_count,
type: :number,
title: I18n.t("reports.moderators_activity.labels.revision_count") title: I18n.t("reports.moderators_activity.labels.revision_count")
} }
] ]

View File

@ -850,6 +850,7 @@ en:
default: default:
labels: labels:
count: Count count: Count
percent: Percent
day: Day day: Day
post_edits: post_edits:
title: "Post edits" title: "Post edits"

View File

@ -418,7 +418,7 @@ QUnit.test("computed labels", assert => {
}, },
title: "Moderator" title: "Moderator"
}, },
{ property: "flag_count", title: "Flag count" }, { type: "number", property: "flag_count", title: "Flag count" },
{ type: "seconds", property: "time_read", title: "Time read" }, { type: "seconds", property: "time_read", title: "Time read" },
{ type: "text", property: "note", title: "Note" }, { type: "text", property: "note", title: "Note" },
{ {
@ -453,62 +453,66 @@ QUnit.test("computed labels", assert => {
assert.equal(usernameLabel.mainProperty, "username"); assert.equal(usernameLabel.mainProperty, "username");
assert.equal(usernameLabel.sortProperty, "username"); assert.equal(usernameLabel.sortProperty, "username");
assert.equal(usernameLabel.title, "Moderator"); assert.equal(usernameLabel.title, "Moderator");
assert.equal(usernameLabel.type, "user");
const computedUsernameLabel = usernameLabel.compute(row); const computedUsernameLabel = usernameLabel.compute(row);
assert.equal( assert.equal(
computedUsernameLabel.formatedValue, computedUsernameLabel.formatedValue,
"<a href='/admin/users/1/joffrey'><img alt='' width='20' height='20' src='/' class='avatar' title='joffrey'><span class='username'>joffrey</span></a>" "<a href='/admin/users/1/joffrey'><img alt='' width='20' height='20' src='/' class='avatar' title='joffrey'><span class='username'>joffrey</span></a>"
); );
assert.equal(computedUsernameLabel.type, "user");
assert.equal(computedUsernameLabel.value, "joffrey"); assert.equal(computedUsernameLabel.value, "joffrey");
const flagCountLabel = computedLabels[1]; const flagCountLabel = computedLabels[1];
assert.equal(flagCountLabel.mainProperty, "flag_count"); assert.equal(flagCountLabel.mainProperty, "flag_count");
assert.equal(flagCountLabel.sortProperty, "flag_count"); assert.equal(flagCountLabel.sortProperty, "flag_count");
assert.equal(flagCountLabel.title, "Flag count"); assert.equal(flagCountLabel.title, "Flag count");
const computedFlagCountLabel = flagCountLabel.compute(row); assert.equal(flagCountLabel.type, "number");
let computedFlagCountLabel = flagCountLabel.compute(row);
assert.equal(computedFlagCountLabel.formatedValue, "1.9k"); assert.equal(computedFlagCountLabel.formatedValue, "1.9k");
assert.equal(computedFlagCountLabel.type, "number");
assert.equal(computedFlagCountLabel.value, 1876); assert.equal(computedFlagCountLabel.value, 1876);
computedFlagCountLabel = flagCountLabel.compute(row, {
formatNumbers: false
});
assert.equal(computedFlagCountLabel.formatedValue, 1876);
const timeReadLabel = computedLabels[2]; const timeReadLabel = computedLabels[2];
assert.equal(timeReadLabel.mainProperty, "time_read"); assert.equal(timeReadLabel.mainProperty, "time_read");
assert.equal(timeReadLabel.sortProperty, "time_read"); assert.equal(timeReadLabel.sortProperty, "time_read");
assert.equal(timeReadLabel.title, "Time read"); assert.equal(timeReadLabel.title, "Time read");
assert.equal(timeReadLabel.type, "seconds");
const computedTimeReadLabel = timeReadLabel.compute(row); const computedTimeReadLabel = timeReadLabel.compute(row);
assert.equal(computedTimeReadLabel.formatedValue, "3d"); assert.equal(computedTimeReadLabel.formatedValue, "3d");
assert.equal(computedTimeReadLabel.type, "seconds");
assert.equal(computedTimeReadLabel.value, 287362); assert.equal(computedTimeReadLabel.value, 287362);
const noteLabel = computedLabels[3]; const noteLabel = computedLabels[3];
assert.equal(noteLabel.mainProperty, "note"); assert.equal(noteLabel.mainProperty, "note");
assert.equal(noteLabel.sortProperty, "note"); assert.equal(noteLabel.sortProperty, "note");
assert.equal(noteLabel.title, "Note"); assert.equal(noteLabel.title, "Note");
assert.equal(noteLabel.type, "text");
const computedNoteLabel = noteLabel.compute(row); const computedNoteLabel = noteLabel.compute(row);
assert.equal(computedNoteLabel.formatedValue, "This is a long note"); assert.equal(computedNoteLabel.formatedValue, "This is a long note");
assert.equal(computedNoteLabel.type, "text");
assert.equal(computedNoteLabel.value, "This is a long note"); assert.equal(computedNoteLabel.value, "This is a long note");
const topicLabel = computedLabels[4]; const topicLabel = computedLabels[4];
assert.equal(topicLabel.mainProperty, "topic_title"); assert.equal(topicLabel.mainProperty, "topic_title");
assert.equal(topicLabel.sortProperty, "topic_title"); assert.equal(topicLabel.sortProperty, "topic_title");
assert.equal(topicLabel.title, "Topic"); assert.equal(topicLabel.title, "Topic");
assert.equal(topicLabel.type, "topic");
const computedTopicLabel = topicLabel.compute(row); const computedTopicLabel = topicLabel.compute(row);
assert.equal( assert.equal(
computedTopicLabel.formatedValue, computedTopicLabel.formatedValue,
"<a href='/t/-/2'>Test topic</a>" "<a href='/t/-/2'>Test topic</a>"
); );
assert.equal(computedTopicLabel.type, "topic");
assert.equal(computedTopicLabel.value, "Test topic"); assert.equal(computedTopicLabel.value, "Test topic");
const postLabel = computedLabels[5]; const postLabel = computedLabels[5];
assert.equal(postLabel.mainProperty, "post_raw"); assert.equal(postLabel.mainProperty, "post_raw");
assert.equal(postLabel.sortProperty, "post_raw"); assert.equal(postLabel.sortProperty, "post_raw");
assert.equal(postLabel.title, "Post"); assert.equal(postLabel.title, "Post");
assert.equal(postLabel.type, "post");
const computedPostLabel = postLabel.compute(row); const computedPostLabel = postLabel.compute(row);
assert.equal( assert.equal(
computedPostLabel.formatedValue, computedPostLabel.formatedValue,
"<a href='/t/-/2/3'>This is the beginning of</a>" "<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"); assert.equal(computedPostLabel.value, "This is the beginning of");
}); });