diff --git a/.travis.yml b/.travis.yml
index ba9f8a2c07..16ccc5722b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,15 @@
language: ruby
+branches:
+ only:
+ - master
+ - beta
+ - stable
+
env:
global:
- DISCOURSE_HOSTNAME=www.example.com
- - RUBY_GC_MALLOC_LIMIT=50000000
+ - RUBY_GLOBAL_METHOD_CACHE_SIZE=131072
matrix:
- "RAILS_MASTER=0 QUNIT_RUN=0 RUN_LINT=0"
- "RAILS_MASTER=0 QUNIT_RUN=1 RUN_LINT=0"
@@ -11,8 +17,9 @@ env:
addons:
chrome: stable
- postgresql: 9.5
+ postgresql: 9.6
apt:
+ update: true
packages:
- gifsicle
- jpegoptim
@@ -33,6 +40,7 @@ sudo: required
dist: trusty
cache:
+ apt: true
yarn: true
directories:
- vendor/bundle
@@ -50,10 +58,11 @@ before_install:
- export PATH=$HOME/.yarn/bin:$PATH
install:
- - bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu; fi"
- - bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3; fi"
- - bash -c "if [ '$RUN_LINT' == '1' ]; then yarn global add eslint babel-eslint; fi"
- - bash -c "if [ '$QUNIT_RUN' == '1' ]; then yarn install --dev; fi"
+ - bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu > /dev/null; fi"
+ - bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3 > /dev/null; fi"
+ - bash -c "if [ '$RUN_LINT' == '1' ]; then yarn global add eslint babel-eslint > /dev/null; fi"
+ - bash -c "if [ '$QUNIT_RUN' == '1' ]; then yarn install --dev > /dev/null; fi"
+ - bash -c "if [ '$RUN_LINT' != '1' ]; then bundle exec rake db:create db:migrate > /dev/null; fi"
script:
- |
@@ -66,8 +75,6 @@ script:
eslint --ext .es6 plugins/**/test/javascripts && \
eslint app/assets/javascripts test/javascripts
else
- bundle exec rake db:create db:migrate
-
if [ '$QUNIT_RUN' == '1' ]; then
bundle exec rake qunit:test['500000'] && \
bundle exec rake plugin:qunit
diff --git a/Gemfile b/Gemfile
index c89be78a7c..f5a03d7d5d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -34,7 +34,7 @@ gem 'redis-namespace'
gem 'active_model_serializers', '~> 0.8.3'
-gem 'onebox', '1.8.47'
+gem 'onebox', '1.8.48'
gem 'http_accept_language', '~>2.0.5', require: false
@@ -58,7 +58,7 @@ gem 'aws-sdk-s3', require: false
gem 'excon', require: false
gem 'unf', require: false
-gem 'email_reply_trimmer', '0.1.11'
+gem 'email_reply_trimmer', '~> 0.1'
# Forked until https://github.com/toy/image_optim/pull/149 is merged
gem 'discourse_image_optim', require: 'image_optim'
@@ -115,7 +115,7 @@ group :test, :development do
gem 'listen', require: false
gem 'certified', require: false
# later appears to break Fabricate(:topic, category: category)
- gem 'fabrication', '2.9.8', require: false
+ gem 'fabrication', require: false
gem 'mocha', require: false
gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false
gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false
diff --git a/Gemfile.lock b/Gemfile.lock
index c5b26bd87c..50f5b5c693 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -89,7 +89,7 @@ GEM
image_size (~> 1.5)
in_threads (~> 1.3)
progress (~> 3.0, >= 3.0.1)
- email_reply_trimmer (0.1.11)
+ email_reply_trimmer (0.1.12)
ember-data-source (2.2.1)
ember-source (>= 1.8, < 3.0)
ember-handlebars-template (0.7.5)
@@ -225,7 +225,7 @@ GEM
omniauth-twitter (1.3.0)
omniauth-oauth (~> 1.1)
rack
- onebox (1.8.47)
+ onebox (1.8.48)
htmlentities (~> 4.3)
moneta (~> 1.0)
multi_json (~> 1.11)
@@ -415,13 +415,13 @@ DEPENDENCIES
certified
cppjieba_rb
discourse_image_optim
- email_reply_trimmer (= 0.1.11)
+ email_reply_trimmer (~> 0.1)
ember-handlebars-template (= 0.7.5)
ember-rails (= 0.18.5)
ember-source (= 2.13.3)
excon
execjs
- fabrication (= 2.9.8)
+ fabrication
fakeweb (~> 1.3.0)
fast_blank
fast_xor
@@ -461,7 +461,7 @@ DEPENDENCIES
omniauth-oauth2
omniauth-openid
omniauth-twitter
- onebox (= 1.8.47)
+ onebox (= 1.8.48)
openid-redis-store
pg (~> 0.21.0)
pry-nav
diff --git a/app/assets/javascripts/admin/components/admin-graph.js.es6 b/app/assets/javascripts/admin/components/admin-graph.js.es6
index c99c7b186b..724e967374 100644
--- a/app/assets/javascripts/admin/components/admin-graph.js.es6
+++ b/app/assets/javascripts/admin/components/admin-graph.js.es6
@@ -1,4 +1,5 @@
import loadScript from 'discourse/lib/load-script';
+import { number } from 'discourse/lib/formatter';
export default Ember.Component.extend({
tagName: 'canvas',
@@ -22,10 +23,16 @@ export default Ember.Component.extend({
data: data,
options: {
responsive: true,
+ tooltips: {
+ callbacks: {
+ title: (context) => moment(context[0].xLabel, "YYYY-MM-DD").format("LL")
+ }
+ },
scales: {
yAxes: [{
display: true,
ticks: {
+ callback: (label) => number(label),
suggestedMin: 0
}
}]
diff --git a/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 b/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6
index 82eceb6691..8fae065f7b 100644
--- a/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6
+++ b/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6
@@ -1,32 +1,13 @@
import { ajax } from "discourse/lib/ajax";
-import Report from "admin/models/report";
import AsyncReport from "admin/mixins/async-report";
export default Ember.Component.extend(AsyncReport, {
- classNames: ["dashboard-table", "dashboard-inline-table", "fixed"],
- help: null,
- helpPage: null,
-
- loadReport(report_json) {
- return Report.create(report_json);
- },
+ classNames: ["dashboard-inline-table"],
fetchReport() {
this._super();
- let payload = { data: { cache: true, facets: ["total", "prev30Days"] } };
-
- if (this.get("startDate")) {
- payload.data.start_date = this.get("startDate").format("YYYY-MM-DD[T]HH:mm:ss.SSSZZ");
- }
-
- if (this.get("endDate")) {
- payload.data.end_date = this.get("endDate").format("YYYY-MM-DD[T]HH:mm:ss.SSSZZ");
- }
-
- if (this.get("limit")) {
- payload.data.limit = this.get("limit");
- }
+ let payload = this.buildPayload(["total", "prev30Days"]);
return Ember.RSVP.Promise.all(this.get("dataSources").map(dataSource => {
return ajax(dataSource, payload)
diff --git a/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 b/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6
index 0ac44e2bbb..5992e47953 100644
--- a/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6
+++ b/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6
@@ -52,17 +52,7 @@ export default Ember.Component.extend(AsyncReport, {
fetchReport() {
this._super();
- let payload = {
- data: { cache: true, facets: ["prev_period"] }
- };
-
- if (this.get("startDate")) {
- payload.data.start_date = this.get("startDate").locale('en').format('YYYY-MM-DD[T]HH:mm:ss.SSSZZ');
- }
-
- if (this.get("endDate")) {
- payload.data.end_date = this.get("endDate").locale('en').format('YYYY-MM-DD[T]HH:mm:ss.SSSZZ');
- }
+ let payload = this.buildPayload(["prev_period"]);
if (this._chart) {
this._chart.destroy();
@@ -110,7 +100,7 @@ export default Ember.Component.extend(AsyncReport, {
labels,
datasets: reportsForPeriod.map(report => {
return {
- data: Ember.makeArray(report.data).map(d => d.y),
+ data: Ember.makeArray(report.data).map(d => number(d.y, { ceil: true })),
backgroundColor: "rgba(200,220,240,0.3)",
borderColor: report.color
};
@@ -157,7 +147,7 @@ export default Ember.Component.extend(AsyncReport, {
scales: {
yAxes: [{
display: true,
- ticks: { callback: (label) => number(label) }
+ ticks: { callback: (label) => number(label, { ceil: true }) }
}],
xAxes: [{
display: true,
diff --git a/app/assets/javascripts/admin/components/dashboard-table.js.es6 b/app/assets/javascripts/admin/components/dashboard-table.js.es6
new file mode 100644
index 0000000000..96b74e6e15
--- /dev/null
+++ b/app/assets/javascripts/admin/components/dashboard-table.js.es6
@@ -0,0 +1,19 @@
+import { ajax } from "discourse/lib/ajax";
+import AsyncReport from "admin/mixins/async-report";
+
+export default Ember.Component.extend(AsyncReport, {
+ classNames: ["dashboard-table"],
+
+ fetchReport() {
+ this._super();
+
+ let payload = this.buildPayload(["total", "prev30Days"]);
+
+ return Ember.RSVP.Promise.all(this.get("dataSources").map(dataSource => {
+ return ajax(dataSource, payload)
+ .then(response => {
+ this.get("reports").pushObject(this.loadReport(response.report));
+ });
+ }));
+ }
+});
diff --git a/app/assets/javascripts/admin/components/watched-word-form.js.es6 b/app/assets/javascripts/admin/components/watched-word-form.js.es6
index 9b2213de61..e7ead641f2 100644
--- a/app/assets/javascripts/admin/components/watched-word-form.js.es6
+++ b/app/assets/javascripts/admin/components/watched-word-form.js.es6
@@ -5,7 +5,7 @@ export default Ember.Component.extend({
classNames: ['watched-word-form'],
formSubmitted: false,
actionKey: null,
- showSuccessMessage: false,
+ showMessage: false,
@computed('regularExpressions')
placeholderKey(regularExpressions) {
@@ -14,21 +14,33 @@ export default Ember.Component.extend({
},
@observes('word')
- removeSuccessMessage() {
- if (this.get('showSuccessMessage') && !Ember.isEmpty(this.get('word'))) {
- this.set('showSuccessMessage', false);
+ removeMessage() {
+ if (this.get('showMessage') && !Ember.isEmpty(this.get('word'))) {
+ this.set('showMessage', false);
}
},
+ @computed('word')
+ isUniqueWord(word) {
+ const words = this.get("filteredContent") || [];
+ const filtered = words.filter(content => content.action === this.get("actionKey"));
+ return filtered.every(content => content.word.toLowerCase() !== word.toLowerCase());
+ },
+
actions: {
submit() {
+ if (!this.get("isUniqueWord")) {
+ this.setProperties({ showMessage: true, message: I18n.t('admin.watched_words.form.exists') });
+ return;
+ }
+
if (!this.get('formSubmitted')) {
this.set('formSubmitted', true);
const watchedWord = WatchedWord.create({ word: this.get('word'), action: this.get('actionKey') });
watchedWord.save().then(result => {
- this.setProperties({ word: '', formSubmitted: false, showSuccessMessage: true });
+ this.setProperties({ word: '', formSubmitted: false, showMessage: true, message: I18n.t('admin.watched_words.form.success') });
this.sendAction('action', WatchedWord.create(result));
Ember.run.schedule('afterRender', () => this.$('.watched-word-input').focus());
}).catch(e => {
diff --git a/app/assets/javascripts/admin/controllers/admin-reports.js.es6 b/app/assets/javascripts/admin/controllers/admin-reports.js.es6
index 83fde41c7e..1b02d5575a 100644
--- a/app/assets/javascripts/admin/controllers/admin-reports.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-reports.js.es6
@@ -4,6 +4,7 @@ import Report from 'admin/models/report';
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
+ classNames: ["admin-reports"],
queryParams: ["mode", "start_date", "end_date", "category_id", "group_id"],
viewMode: 'graph',
viewingTable: Em.computed.equal('viewMode', 'table'),
diff --git a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6
index ee49a1c119..0d2ae55508 100644
--- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6
@@ -50,6 +50,36 @@ export default Ember.Controller.extend(CanCheckEmails, {
return userPath(`${username}/preferences`);
},
+ @computed('model.can_delete_all_posts', 'model.staff', 'model.post_count')
+ deleteAllPostsExplanation(canDeleteAllPosts, staff, postCount) {
+ if (canDeleteAllPosts) {
+ return null;
+ }
+
+ if (staff) {
+ return I18n.t('admin.user.delete_posts_forbidden_because_staff');
+ }
+ if (postCount > this.siteSettings.delete_all_posts_max) {
+ return I18n.t('admin.user.cant_delete_all_too_many_posts', {count: this.siteSettings.delete_all_posts_max});
+ } else {
+ return I18n.t('admin.user.cant_delete_all_posts', {count: this.siteSettings.delete_user_max_post_age});
+ }
+ },
+
+ @computed('model.canBeDeleted', 'model.staff')
+ deleteExplanation(canBeDeleted, staff) {
+ if (canBeDeleted) {
+ return null;
+ }
+
+ if (staff) {
+ return I18n.t('admin.user.delete_forbidden_because_staff');
+ } else {
+ return I18n.t('admin.user.delete_forbidden', {count: this.siteSettings.delete_user_max_post_age});
+ }
+ },
+
+
actions: {
impersonate() { return this.get("model").impersonate(); },
@@ -73,6 +103,16 @@ export default Ember.Controller.extend(CanCheckEmails, {
anonymize() { return this.get('model').anonymize(); },
disableSecondFactor() { return this.get('model').disableSecondFactor(); },
+
+ clearPenaltyHistory() {
+ let user = this.get('model');
+ return ajax(`/admin/users/${user.get('id')}/penalty_history`, {
+ type: 'DELETE'
+ }).then(() => {
+ user.set('tl3_requirements.penalty_counts.total', 0);
+ }).catch(popupAjaxError);
+ },
+
destroy() {
const postCount = this.get('model.post_count');
if (postCount <= 5) {
diff --git a/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6 b/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6
index dc4428fc72..7f9bc8fd86 100644
--- a/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6
@@ -46,6 +46,10 @@ export default Ember.Controller.extend({
actions: {
clearFilter() {
this.setProperties({ filter: '' });
+ },
+
+ toggleMenu() {
+ $('.admin-detail').toggleClass('mobile-closed mobile-open');
}
}
diff --git a/app/assets/javascripts/admin/mixins/async-report.js.es6 b/app/assets/javascripts/admin/mixins/async-report.js.es6
index 2c22ab15c2..760a205948 100644
--- a/app/assets/javascripts/admin/mixins/async-report.js.es6
+++ b/app/assets/javascripts/admin/mixins/async-report.js.es6
@@ -1,7 +1,8 @@
import computed from "ember-addons/ember-computed-decorators";
+import Report from "admin/models/report";
export default Ember.Mixin.create({
- classNameBindings: ["isLoading"],
+ classNameBindings: ["isLoading", "dataSourceNames"],
reports: null,
isLoading: false,
dataSourceNames: "",
@@ -17,6 +18,24 @@ export default Ember.Mixin.create({
return dataSourceNames.split(",").map(source => `/admin/reports/${source}`);
},
+ buildPayload(facets) {
+ let payload = { data: { cache: true, facets } };
+
+ if (this.get("startDate")) {
+ payload.data.start_date = this.get("startDate").format("YYYY-MM-DD[T]HH:mm:ss.SSSZZ");
+ }
+
+ if (this.get("endDate")) {
+ payload.data.end_date = this.get("endDate").format("YYYY-MM-DD[T]HH:mm:ss.SSSZZ");
+ }
+
+ if (this.get("limit")) {
+ payload.data.limit = this.get("limit");
+ }
+
+ return payload;
+ },
+
@computed("reports.[]", "startDate", "endDate", "dataSourceNames")
reportsForPeriod(reports, startDate, endDate, dataSourceNames) {
// on a slow network fetchReport could be called multiple times between
@@ -25,7 +44,6 @@ export default Ember.Mixin.create({
// the array contains only unique values
reports = reports.uniqBy("report_key");
-
const sort = (r) => {
if (r.length > 1) {
return dataSourceNames
@@ -40,7 +58,6 @@ export default Ember.Mixin.create({
return sort(reports);
}
-
return sort(reports.filter(report => {
return report.report_key.includes(startDate.format("YYYYMMDD")) &&
report.report_key.includes(endDate.format("YYYYMMDD"));
@@ -71,7 +88,9 @@ export default Ember.Mixin.create({
this.set("isLoading", false);
},
- loadReport() {},
+ loadReport(jsonReport) {
+ return Report.create(jsonReport);
+ },
fetchReport() {
this.set("reports", []);
diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6
index b826426473..ba7528258d 100644
--- a/app/assets/javascripts/admin/models/admin-user.js.es6
+++ b/app/assets/javascripts/admin/models/admin-user.js.es6
@@ -87,21 +87,6 @@ const AdminUser = Discourse.User.extend({
}).then(() => this.set('api_key', null));
},
- deleteAllPostsExplanation: function() {
- if (!this.get('can_delete_all_posts')) {
- if (this.get('deleteForbidden') && this.get('staff')) {
- return I18n.t('admin.user.delete_posts_forbidden_because_staff');
- }
- if (this.get('post_count') > Discourse.SiteSettings.delete_all_posts_max) {
- return I18n.t('admin.user.cant_delete_all_too_many_posts', {count: Discourse.SiteSettings.delete_all_posts_max});
- } else {
- return I18n.t('admin.user.cant_delete_all_posts', {count: Discourse.SiteSettings.delete_user_max_post_age});
- }
- } else {
- return null;
- }
- }.property('can_delete_all_posts', 'deleteForbidden'),
-
deleteAllPosts() {
const user = this,
message = I18n.messageFormat('admin.user.delete_all_posts_confirm_MF', { "POSTS": user.get('post_count'), "TOPICS": user.get('topic_count') }),
@@ -240,8 +225,6 @@ const AdminUser = Discourse.User.extend({
return this.get('trust_level') < 4;
}.property('trust_level'),
- isSuspended: Em.computed.equal('suspended', true),
- isSilenced: Ember.computed.equal('silenced', true),
canSuspend: Em.computed.not('staff'),
suspendDuration: function() {
@@ -353,8 +336,6 @@ const AdminUser = Discourse.User.extend({
}).catch(popupAjaxError);
},
- anonymizeForbidden: Em.computed.not("can_be_anonymized"),
-
anonymize() {
const user = this,
message = I18n.t("admin.user.anonymize_confirm");
@@ -393,20 +374,6 @@ const AdminUser = Discourse.User.extend({
bootbox.dialog(message, buttons, { "classes": "delete-user-modal" });
},
- deleteForbidden: Em.computed.not("canBeDeleted"),
-
- deleteExplanation: function() {
- if (this.get('deleteForbidden')) {
- if (this.get('staff')) {
- return I18n.t('admin.user.delete_forbidden_because_staff');
- } else {
- return I18n.t('admin.user.delete_forbidden', {count: Discourse.SiteSettings.delete_user_max_post_age});
- }
- } else {
- return null;
- }
- }.property('deleteForbidden'),
-
destroy(opts) {
const user = this,
message = I18n.t("admin.user.delete_confirm"),
diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6
index 5441be004a..80c160a8d9 100644
--- a/app/assets/javascripts/admin/models/report.js.es6
+++ b/app/assets/javascripts/admin/models/report.js.es6
@@ -1,22 +1,24 @@
-import { ajax } from 'discourse/lib/ajax';
+import { ajax } from "discourse/lib/ajax";
import round from "discourse/lib/round";
-import { fillMissingDates } from 'discourse/lib/utilities';
-import computed from 'ember-addons/ember-computed-decorators';
+import { fillMissingDates } from "discourse/lib/utilities";
+import computed from "ember-addons/ember-computed-decorators";
+import { number } from 'discourse/lib/formatter';
const Report = Discourse.Model.extend({
average: false,
percent: false,
+ higher_is_better: true,
@computed("type", "start_date", "end_date")
reportUrl(type, start_date, end_date) {
- start_date = moment(start_date).locale('en').format("YYYY-MM-DD");
- end_date = moment(end_date).locale('en').format("YYYY-MM-DD");
+ start_date = moment(start_date).locale("en").format("YYYY-MM-DD");
+ end_date = moment(end_date).locale("en").format("YYYY-MM-DD");
return Discourse.getURL(`/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}`);
},
valueAt(numDaysAgo) {
if (this.data) {
- const wantedDate = moment().subtract(numDaysAgo, "days").locale('en').format("YYYY-MM-DD");
+ const wantedDate = moment().subtract(numDaysAgo, "days").locale("en").format("YYYY-MM-DD");
const item = this.data.find(d => d.x === wantedDate);
if (item) {
return item.y;
@@ -29,7 +31,7 @@ const Report = Discourse.Model.extend({
if (this.data) {
const earliestDate = moment().subtract(endDaysAgo, "days").startOf("day");
const latestDate = moment().subtract(startDaysAgo, "days").startOf("day");
- var d, sum = 0, count = 0;
+ let d, sum = 0, count = 0;
_.each(this.data, datum => {
d = moment(datum.x);
if (d >= earliestDate && d <= latestDate) {
@@ -46,6 +48,7 @@ const Report = Discourse.Model.extend({
yesterdayCount: function() { return this.valueAt(1); }.property("data", "average"),
sevenDaysAgoCount: function() { return this.valueAt(7); }.property("data", "average"),
thirtyDaysAgoCount: function() { return this.valueAt(30); }.property("data", "average"),
+
lastSevenDaysCount: function() {
return this.averageCount(7, this.valueFor(1, 7));
}.property("data", "average"),
@@ -57,112 +60,66 @@ const Report = Discourse.Model.extend({
return this.get("average") ? value / count : value;
},
- @computed('yesterdayCount')
- yesterdayTrend(yesterdayCount) {
- const yesterdayVal = yesterdayCount;
- const twoDaysAgoVal = this.valueAt(2);
- const change = ((yesterdayVal - twoDaysAgoVal) / yesterdayVal) * 100;
-
- if (change > 50) {
- return "high-trending-up";
- } else if (change > 0) {
- return "trending-up";
- } else if (change === 0) {
- return "no-change";
- } else if (change < -50) {
- return "high-trending-down";
- } else if (change < 0) {
- return "trending-down";
- }
+ @computed("yesterdayCount", "higher_is_better")
+ yesterdayTrend(yesterdayCount, higherIsBetter) {
+ return this._computeTrend(this.valueAt(2), yesterdayCount, higherIsBetter);
},
- @computed('lastSevenDaysCount')
- sevenDayTrend(lastSevenDaysCount) {
- const currentPeriod = lastSevenDaysCount;
- const prevPeriod = this.valueFor(8, 14);
- const change = ((currentPeriod - prevPeriod) / prevPeriod) * 100;
-
- if (change > 50) {
- return "high-trending-up";
- } else if (change > 0) {
- return "trending-up";
- } else if (change === 0) {
- return "no-change";
- } else if (change < -50) {
- return "high-trending-down";
- } else if (change < 0) {
- return "trending-down";
- }
+ @computed("lastSevenDaysCount", "higher_is_better")
+ sevenDaysTrend(lastSevenDaysCount, higherIsBetter) {
+ return this._computeTrend(this.valueFor(8, 14), lastSevenDaysCount, higherIsBetter);
},
- @computed('data')
+ @computed("data")
currentTotal(data){
return _.reduce(data, (cur, pair) => cur + pair.y, 0);
},
- @computed('data', 'currentTotal')
+ @computed("data", "currentTotal")
currentAverage(data, total) {
return Ember.makeArray(data).length === 0 ? 0 : parseFloat((total / parseFloat(data.length)).toFixed(1));
},
- @computed("trend")
- trendIcon(trend) {
- switch (trend) {
- case "trending-up":
- return "angle-up";
- case "trending-down":
- return "angle-down";
- case "high-trending-up":
- return "angle-double-up";
- case "high-trending-down":
- return "angle-double-down";
- default:
- return null;
- }
+ @computed("trend", "higher_is_better")
+ trendIcon(trend, higherIsBetter) {
+ return this._iconForTrend(trend, higherIsBetter);
},
- @computed('prev_period', 'currentTotal', 'currentAverage')
- trend(prev, currentTotal, currentAverage) {
- const total = this.get('average') ? currentAverage : currentTotal;
- const change = ((total - prev) / total) * 100;
-
- if (change > 50) {
- return "high-trending-up";
- } else if (change > 0) {
- return "trending-up";
- } else if (change === 0) {
- return "no-change";
- } else if (change < -50) {
- return "high-trending-down";
- } else if (change < 0) {
- return "trending-down";
- }
+ @computed("sevenDaysTrend", "higher_is_better")
+ sevenDaysTrendIcon(sevenDaysTrend, higherIsBetter) {
+ return this._iconForTrend(sevenDaysTrend, higherIsBetter);
},
- @computed('prev30Days', 'lastThirtyDaysCount')
- thirtyDayTrend(prev30Days, lastThirtyDaysCount) {
- const currentPeriod = lastThirtyDaysCount;
- const change = ((currentPeriod - prev30Days) / currentPeriod) * 100;
-
- if (change > 50) {
- return "high-trending-up";
- } else if (change > 0) {
- return "trending-up";
- } else if (change === 0) {
- return "no-change";
- } else if (change < -50) {
- return "high-trending-down";
- } else if (change < 0) {
- return "trending-down";
- }
+ @computed("thirtyDaysTrend", "higher_is_better")
+ thirtyDaysTrendIcon(thirtyDaysTrend, higherIsBetter) {
+ return this._iconForTrend(thirtyDaysTrend, higherIsBetter);
},
- @computed('type')
+ @computed("yesterdayTrend", "higher_is_better")
+ yesterdayTrendIcon(yesterdayTrend, higherIsBetter) {
+ return this._iconForTrend(yesterdayTrend, higherIsBetter);
+ },
+
+ @computed("prev_period", "currentTotal", "currentAverage", "higher_is_better")
+ trend(prev, currentTotal, currentAverage, higherIsBetter) {
+ const total = this.get("average") ? currentAverage : currentTotal;
+ return this._computeTrend(prev, total, higherIsBetter);
+ },
+
+ @computed("prev30Days", "lastThirtyDaysCount", "higher_is_better")
+ thirtyDaysTrend(prev30Days, lastThirtyDaysCount, higherIsBetter) {
+ return this._computeTrend(prev30Days, lastThirtyDaysCount, higherIsBetter);
+ },
+
+ @computed("type")
icon(type) {
if (type.indexOf("message") > -1) {
return "envelope";
}
switch (type) {
+ case "page_view_total_reqs": return "file";
+ case "visits": return "user";
+ case "time_to_first_response": return "reply";
case "flags": return "flag";
case "likes": return "heart";
case "bookmarks": return "bookmark";
@@ -170,7 +127,7 @@ const Report = Discourse.Model.extend({
}
},
- @computed('type')
+ @computed("type")
method(type) {
if (type === "time_to_first_response") {
return "average";
@@ -180,75 +137,112 @@ const Report = Discourse.Model.extend({
},
percentChangeString(val1, val2) {
- const val = ((val1 - val2) / val2) * 100;
- if (isNaN(val) || !isFinite(val)) {
+ const change = this._computeChange(val1, val2);
+
+ if (isNaN(change) || !isFinite(change)) {
return null;
- } else if (val > 0) {
- return "+" + val.toFixed(0) + "%";
+ } else if (change > 0) {
+ return "+" + change.toFixed(0) + "%";
} else {
- return val.toFixed(0) + "%";
+ return change.toFixed(0) + "%";
}
},
- @computed('prev_period', 'currentTotal', 'currentAverage')
+ @computed("prev_period", "currentTotal", "currentAverage")
trendTitle(prev, currentTotal, currentAverage) {
- let current = this.get('average') ? currentAverage : currentTotal;
- let percent = this.percentChangeString(current, prev);
+ let current = this.get("average") ? currentAverage : currentTotal;
+ let percent = this.percentChangeString(prev, current);
- if (this.get('average')) {
+ if (this.get("average")) {
prev = prev ? prev.toFixed(1) : "0";
- if (this.get('percent')) {
- current += '%';
- prev += '%';
+ if (this.get("percent")) {
+ current += "%";
+ prev += "%";
}
+ } else {
+ prev = number(prev);
+ current = number(current);
}
- return I18n.t('admin.dashboard.reports.trend_title', {percent: percent, prev: prev, current: current});
+ return I18n.t("admin.dashboard.reports.trend_title", {percent, prev, current});
},
- changeTitle(val1, val2, prevPeriodString) {
- const percentChange = this.percentChangeString(val1, val2);
- var title = "";
- if (percentChange) { title += percentChange + " change. "; }
- title += "Was " + val2 + " " + prevPeriodString + ".";
+ changeTitle(valAtT1, valAtT2, prevPeriodString) {
+ const change = this.percentChangeString(valAtT1, valAtT2);
+ let title = "";
+ if (change) { title += `${change} change. `; }
+ title += `Was ${number(valAtT1)} ${prevPeriodString}.`;
return title;
},
- @computed('yesterdayCount')
+ @computed("yesterdayCount")
yesterdayCountTitle(yesterdayCount) {
- return this.changeTitle(yesterdayCount, this.valueAt(2), "two days ago");
+ return this.changeTitle(this.valueAt(2), yesterdayCount, "two days ago");
},
- @computed('lastSevenDaysCount')
- sevenDayCountTitle(lastSevenDaysCount) {
- return this.changeTitle(lastSevenDaysCount, this.valueFor(8, 14), "two weeks ago");
+ @computed("lastSevenDaysCount")
+ sevenDaysCountTitle(lastSevenDaysCount) {
+ return this.changeTitle(this.valueFor(8, 14), lastSevenDaysCount, "two weeks ago");
},
- @computed('prev30Days', 'lastThirtyDaysCount')
- thirtyDayCountTitle(prev30Days, lastThirtyDaysCount) {
- return this.changeTitle(lastThirtyDaysCount, prev30Days, "in the previous 30 day period");
+ @computed("prev30Days", "lastThirtyDaysCount")
+ thirtyDaysCountTitle(prev30Days, lastThirtyDaysCount) {
+ return this.changeTitle(prev30Days, lastThirtyDaysCount, "in the previous 30 day period");
},
- @computed('data')
+ @computed("data")
sortedData(data) {
- return this.get('xAxisIsDate') ? data.toArray().reverse() : data.toArray();
+ return this.get("xAxisIsDate") ? data.toArray().reverse() : data.toArray();
},
- @computed('data')
+ @computed("data")
xAxisIsDate() {
if (!this.data[0]) return false;
return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/);
- }
+ },
+ _computeChange(valAtT1, valAtT2) {
+ return ((valAtT2 - valAtT1) / valAtT1) * 100;
+ },
+
+ _computeTrend(valAtT1, valAtT2, higherIsBetter) {
+ const change = this._computeChange(valAtT1, valAtT2);
+
+ if (change > 50) {
+ return higherIsBetter ? "high-trending-up" : "high-trending-down";
+ } else if (change > 2) {
+ return higherIsBetter ? "trending-up" : "trending-down";
+ } else if (change <= 2 && change >= -2) {
+ return "no-change";
+ } else if (change < -50) {
+ return higherIsBetter ? "high-trending-down" : "high-trending-up";
+ } else if (change < -2) {
+ return higherIsBetter ? "trending-down" : "trending-up";
+ }
+ },
+
+ _iconForTrend(trend, higherIsBetter) {
+ switch (trend) {
+ case "trending-up":
+ return higherIsBetter ? "angle-up" : "angle-down";
+ case "trending-down":
+ return higherIsBetter ? "angle-down" : "angle-up";
+ case "high-trending-up":
+ return higherIsBetter ? "angle-double-up" : "angle-double-down";
+ case "high-trending-down":
+ return higherIsBetter ? "angle-double-down" : "angle-double-up";
+ default:
+ return null;
+ }
+ }
});
Report.reopenClass({
-
fillMissingDates(report) {
if (_.isArray(report.data)) {
- const startDateFormatted = moment.utc(report.start_date).locale('en').format('YYYY-MM-DD');
- const endDateFormatted = moment.utc(report.end_date).locale('en').format('YYYY-MM-DD');
+ const startDateFormatted = moment.utc(report.start_date).locale("en").format("YYYY-MM-DD");
+ const endDateFormatted = moment.utc(report.end_date).locale("en").format("YYYY-MM-DD");
report.data = fillMissingDates(report.data, startDateFormatted, endDateFormatted);
}
},
@@ -272,7 +266,7 @@ Report.reopenClass({
// TODO: fillMissingDates if xaxis is date
const related = Report.create({ type: json.report.related_report.type });
related.setProperties(json.report.related_report);
- model.set('relatedReport', related);
+ model.set("relatedReport", related);
}
return model;
diff --git a/app/assets/javascripts/admin/models/staff-action-log.js.es6 b/app/assets/javascripts/admin/models/staff-action-log.js.es6
index 78d39869cf..88cba94b4c 100644
--- a/app/assets/javascripts/admin/models/staff-action-log.js.es6
+++ b/app/assets/javascripts/admin/models/staff-action-log.js.es6
@@ -1,54 +1,58 @@
-import { ajax } from 'discourse/lib/ajax';
-import AdminUser from 'admin/models/admin-user';
-import { escapeExpression } from 'discourse/lib/utilities';
+import computed from "ember-addons/ember-computed-decorators";
+import { ajax } from "discourse/lib/ajax";
+import AdminUser from "admin/models/admin-user";
+import { escapeExpression } from "discourse/lib/utilities";
+
+function format(label, value, escape = true) {
+ return value ? `${I18n.t(label)}: ${escape ? escapeExpression(value) : value}` : "";
+};
const StaffActionLog = Discourse.Model.extend({
showFullDetails: false,
- actionName: function() {
- return I18n.t("admin.logs.staff_actions.actions." + this.get('action_name'));
- }.property('action_name'),
-
- formattedDetails: function() {
- let formatted = "";
- formatted += this.format('email', 'email');
- formatted += this.format('admin.logs.ip_address', 'ip_address');
- formatted += this.format('admin.logs.topic_id', 'topic_id');
- formatted += this.format('admin.logs.post_id', 'post_id');
- formatted += this.format('admin.logs.category_id', 'category_id');
- if (!this.get('useCustomModalForDetails')) {
- formatted += this.format('admin.logs.staff_actions.new_value', 'new_value');
- formatted += this.format('admin.logs.staff_actions.previous_value', 'previous_value');
- }
- if (!this.get('useModalForDetails')) {
- if (this.get('details')) formatted += escapeExpression(this.get('details')) + '
';
- }
- return formatted;
- }.property('ip_address', 'email', 'topic_id', 'post_id', 'category_id'),
-
- format(label, propertyName) {
- if (this.get(propertyName)) {
- let value = escapeExpression(this.get(propertyName));
- if (propertyName === 'post_id') {
- value = `${value}`;
- }
- return `${I18n.t(label)}: ${value}
`;
- } else {
- return '';
- }
+ @computed("action_name")
+ actionName(actionName) {
+ return I18n.t(`admin.logs.staff_actions.actions.${actionName}`);
},
- useModalForDetails: function() {
- return (this.get('details') && this.get('details').length > 100);
- }.property('action_name'),
+ @computed("email", "ip_address", "topic_id", "post_id", "category_id", "new_value", "previous_value", "details", "useCustomModalForDetails", "useModalForDetails")
+ formattedDetails(email, ipAddress, topicId, postId, categoryId, newValue, previousValue, details, useCustomModalForDetails, useModalForDetails) {
+ const postLink = postId ? `${postId}` : null;
- useCustomModalForDetails: function() {
- return _.contains(['change_theme', 'delete_theme'], this.get('action_name'));
- }.property('action_name')
+ let lines = [
+ format("email", email),
+ format("admin.logs.ip_address", ipAddress),
+ format("admin.logs.topic_id", topicId),
+ format("admin.logs.post_id", postLink, false),
+ format("admin.logs.category_id", categoryId),
+ ];
+
+ if (!useCustomModalForDetails) {
+ lines.push(format("admin.logs.staff_actions.new_value", newValue));
+ lines.push(format("admin.logs.staff_actions.previous_value", previousValue));
+ }
+
+ if (!useModalForDetails && details) {
+ lines = [...lines, ...escapeExpression(details).split("\n")];
+ }
+
+ const formatted = lines.filter(l => l.length > 0).join("
");
+ return formatted.length > 0 ? formatted + "
" : "";
+ },
+
+ @computed("details")
+ useModalForDetails(details) {
+ return details && details.length > 100;
+ },
+
+ @computed("action_name")
+ useCustomModalForDetails(actionName) {
+ return ["change_theme", "delete_theme"].includes(actionName);
+ }
});
StaffActionLog.reopenClass({
- create: function(attrs) {
+ create(attrs) {
attrs = attrs || {};
if (attrs.acting_user) {
@@ -60,13 +64,11 @@ StaffActionLog.reopenClass({
return this._super(attrs);
},
- findAll: function(filters) {
- return ajax("/admin/logs/staff_action_logs.json", { data: filters }).then((data) => {
+ findAll(data) {
+ return ajax("/admin/logs/staff_action_logs.json", { data }).then(result => {
return {
- staff_action_logs: data.staff_action_logs.map(function(s) {
- return StaffActionLog.create(s);
- }),
- user_history_actions: data.user_history_actions
+ staff_action_logs: result.staff_action_logs.map(s => StaffActionLog.create(s)),
+ user_history_actions: result.user_history_actions
};
});
}
diff --git a/app/assets/javascripts/admin/templates/badges-show.hbs b/app/assets/javascripts/admin/templates/badges-show.hbs
index 6fa158d0f3..7436c48898 100644
--- a/app/assets/javascripts/admin/templates/badges-show.hbs
+++ b/app/assets/javascripts/admin/templates/badges-show.hbs
@@ -18,7 +18,7 @@
{{i18n 'admin.badges.icon_help'}}
+{{i18n 'admin.badges.image_help'}}
| {{label}} | - {{/each}} - {{else}} - {{#each report.data as |data|}} -{{data.x}} | - {{/each}} - {{/if}} -
|---|---|
| {{number data.y}} | -
| {{label}} | + {{/each}} + {{else}} + {{#each report.data as |data|}} +{{data.x}} | + {{/each}} + {{/if}} +
|---|---|
| {{number data.y}} | +
- {{i18n 'admin.dashboard.find_old'}} {{#link-to 'admin.dashboard'}}{{i18n "admin.dashboard.old_link"}}{{/link-to}} -
- ++ {{i18n 'admin.dashboard.find_old'}} {{#link-to 'admin.dashboard'}}{{i18n "admin.dashboard.old_link"}}{{/link-to}} +
{{/conditional-loading-section}}{{model.description}}
{{/if}} -{{subtitle}}
+ {{/if}} +| - + | {{text-field value=loginName type="email" placeholderKey="login.email_placeholder" id="login-account-name" autocorrect="off" autocapitalize="off"}} @@ -32,10 +32,10 @@ {{/if}} |
| - + | - {{text-field value=loginPassword type="password" id="login-account-password" maxlength="200"}} + {{text-field value=loginPassword type="password" id="login-account-password" maxlength="200"}} |