Version bump
This commit is contained in:
commit
f75dc4ca65
13
.travis.yml
13
.travis.yml
@ -67,8 +67,7 @@ before_install:
|
||||
install:
|
||||
- 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 prettier > /dev/null; fi"
|
||||
- bash -c "if [ '$QUNIT_RUN' == '1' ]; then yarn install --dev > /dev/null; fi"
|
||||
- bash -c "if [ '$QUNIT_RUN' == '1' ] || [ '$RUN_LINT' == '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:
|
||||
@ -77,11 +76,11 @@ script:
|
||||
if [ '$RUN_LINT' == '1' ]; then
|
||||
bundle exec rubocop --parallel && \
|
||||
bundle exec danger && \
|
||||
eslint --ext .es6 app/assets/javascripts && \
|
||||
eslint --ext .es6 test/javascripts && \
|
||||
eslint --ext .es6 plugins/**/assets/javascripts && \
|
||||
eslint --ext .es6 plugins/**/test/javascripts && \
|
||||
eslint app/assets/javascripts test/javascripts
|
||||
yarn eslint --ext .es6 app/assets/javascripts && \
|
||||
yarn eslint --ext .es6 test/javascripts && \
|
||||
yarn eslint --ext .es6 plugins/**/assets/javascripts && \
|
||||
yarn eslint --ext .es6 plugins/**/test/javascripts && \
|
||||
yarn eslint app/assets/javascripts test/javascripts
|
||||
else
|
||||
if [ '$QUNIT_RUN' == '1' ]; then
|
||||
bundle exec rake qunit:test['500000'] && \
|
||||
|
||||
2
Gemfile
2
Gemfile
@ -34,7 +34,7 @@ gem 'redis-namespace'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.8.3'
|
||||
|
||||
gem 'onebox', '1.8.57'
|
||||
gem 'onebox', '1.8.58'
|
||||
|
||||
gem 'http_accept_language', '~>2.0.5', require: false
|
||||
|
||||
|
||||
10
Gemfile.lock
10
Gemfile.lock
@ -257,7 +257,7 @@ GEM
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.8.57)
|
||||
onebox (1.8.58)
|
||||
htmlentities (~> 4.3)
|
||||
moneta (~> 1.0)
|
||||
multi_json (~> 1.11)
|
||||
@ -271,7 +271,7 @@ GEM
|
||||
parallel (1.12.1)
|
||||
parser (2.5.1.0)
|
||||
ast (~> 2.4.0)
|
||||
pg (1.0.0)
|
||||
pg (1.1.0)
|
||||
powerpack (0.1.2)
|
||||
progress (3.4.0)
|
||||
pry (0.10.4)
|
||||
@ -300,7 +300,7 @@ GEM
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.0.4)
|
||||
loofah (~> 2.2, >= 2.2.2)
|
||||
rails_multisite (2.0.5)
|
||||
rails_multisite (2.0.4)
|
||||
activerecord (> 4.2, < 6)
|
||||
railties (> 4.2, < 6)
|
||||
railties (5.2.0)
|
||||
@ -509,7 +509,7 @@ DEPENDENCIES
|
||||
omniauth-oauth2
|
||||
omniauth-openid
|
||||
omniauth-twitter
|
||||
onebox (= 1.8.57)
|
||||
onebox (= 1.8.58)
|
||||
openid-redis-store
|
||||
pg
|
||||
pry-nav
|
||||
@ -554,4 +554,4 @@ DEPENDENCIES
|
||||
webpush
|
||||
|
||||
BUNDLED WITH
|
||||
1.16.3
|
||||
1.16.4
|
||||
|
||||
@ -4,7 +4,6 @@ import loadScript from "discourse/lib/load-script";
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["admin-report-chart"],
|
||||
limit: 8,
|
||||
primaryColor: "rgb(0,136,204)",
|
||||
total: 0,
|
||||
|
||||
willDestroyElement() {
|
||||
@ -38,8 +37,8 @@ export default Ember.Component.extend({
|
||||
data: chartData.map(d => Math.round(parseFloat(d.y))),
|
||||
backgroundColor: prevChartData.length
|
||||
? "transparent"
|
||||
: "rgba(200,220,240,0.3)",
|
||||
borderColor: this.get("primaryColor")
|
||||
: model.secondary_color,
|
||||
borderColor: model.primary_color
|
||||
}
|
||||
]
|
||||
};
|
||||
@ -47,7 +46,7 @@ export default Ember.Component.extend({
|
||||
if (prevChartData.length) {
|
||||
data.datasets.push({
|
||||
data: prevChartData.map(d => Math.round(parseFloat(d.y))),
|
||||
borderColor: this.get("primaryColor"),
|
||||
borderColor: model.primary_color,
|
||||
borderDash: [5, 5],
|
||||
backgroundColor: "transparent",
|
||||
borderWidth: 1,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import ReportLoader from "discourse/lib/reports-loader";
|
||||
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 { SCHEMA_VERSION, default as Report } from "admin/models/report";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import {
|
||||
@ -43,6 +43,7 @@ export default Ember.Component.extend({
|
||||
isEnabled: true,
|
||||
disabledLabel: "admin.dashboard.disabled",
|
||||
isLoading: false,
|
||||
rateLimitationString: null,
|
||||
dataSourceName: null,
|
||||
report: null,
|
||||
model: null,
|
||||
@ -94,7 +95,7 @@ export default Ember.Component.extend({
|
||||
this.get("currentMode")
|
||||
);
|
||||
} else if (this.get("dataSourceName")) {
|
||||
this._fetchReport().finally(() => this._computeReport());
|
||||
this._fetchReport();
|
||||
}
|
||||
},
|
||||
|
||||
@ -303,23 +304,33 @@ export default Ember.Component.extend({
|
||||
_fetchReport() {
|
||||
this._super();
|
||||
|
||||
this.set("isLoading", true);
|
||||
this.setProperties({ isLoading: true, rateLimitationString: null });
|
||||
|
||||
let payload = this._buildPayload(["prev_period"]);
|
||||
Ember.run.next(() => {
|
||||
let payload = this._buildPayload(["prev_period"]);
|
||||
|
||||
return ajax(this.get("dataSource"), payload)
|
||||
.then(response => {
|
||||
if (response && response.report) {
|
||||
this._reports.push(this._loadReport(response.report));
|
||||
} else {
|
||||
console.log("failed loading", this.get("dataSource"));
|
||||
const callback = response => {
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (this.element && !this.isDestroying && !this.isDestroyed) {
|
||||
this.set("isLoading", false);
|
||||
|
||||
this.set("isLoading", false);
|
||||
|
||||
if (response === 429) {
|
||||
this.set(
|
||||
"rateLimitationString",
|
||||
I18n.t("admin.dashboard.too_many_requests")
|
||||
);
|
||||
} else if (response === 500) {
|
||||
this.set("model.error", "exception");
|
||||
} else if (response) {
|
||||
this._reports.push(this._loadReport(response));
|
||||
this._computeReport();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ReportLoader.enqueue(this.get("dataSourceName"), payload.data, callback);
|
||||
});
|
||||
},
|
||||
|
||||
_buildPayload(facets) {
|
||||
|
||||
@ -88,10 +88,13 @@ export default Ember.Component.extend({
|
||||
text += `: ${location.phone}\n`;
|
||||
}
|
||||
}
|
||||
const copyRange = $(`<textarea id="copy-range"></textarea>`);
|
||||
copyRange.text(text);
|
||||
const copyRange = $('<p id="copy-range"></p>');
|
||||
copyRange.html(text.trim().replace("\n", "<br>"));
|
||||
$(document.body).append(copyRange);
|
||||
copyText(text, copyRange[0]);
|
||||
if (copyText(text, copyRange[0])) {
|
||||
this.set("copied", true);
|
||||
Ember.run.later(() => this.set("copied", false), 2000);
|
||||
}
|
||||
copyRange.remove();
|
||||
},
|
||||
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["themes-list"],
|
||||
hasThemes: Ember.computed.gt("themes.length", 0)
|
||||
});
|
||||
@ -53,11 +53,6 @@ export default Ember.Controller.extend({
|
||||
return this.shouldShow("mobile");
|
||||
},
|
||||
|
||||
@computed("onlyOverridden", "model.remote_theme")
|
||||
showSettings() {
|
||||
return false;
|
||||
},
|
||||
|
||||
@observes("onlyOverridden")
|
||||
onlyOverriddenChanged() {
|
||||
if (this.get("onlyOverridden")) {
|
||||
|
||||
@ -8,6 +8,7 @@ import showModal from "discourse/lib/show-modal";
|
||||
import ThemeSettings from "admin/models/theme-settings";
|
||||
|
||||
const THEME_UPLOAD_VAR = 2;
|
||||
const SETTINGS_TYPE_ID = 5;
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
editRouteName: "adminCustomizeThemes.edit",
|
||||
@ -24,8 +25,11 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@computed("model", "allThemes")
|
||||
@computed("model", "allThemes", "model.component")
|
||||
parentThemes(model, allThemes) {
|
||||
if (!model.get("component")) {
|
||||
return null;
|
||||
}
|
||||
let parents = allThemes.filter(theme =>
|
||||
_.contains(theme.get("childThemes"), model)
|
||||
);
|
||||
@ -34,7 +38,9 @@ export default Ember.Controller.extend({
|
||||
|
||||
@computed("model.theme_fields.@each")
|
||||
hasEditedFields(fields) {
|
||||
return fields.any(f => !Em.isBlank(f.value));
|
||||
return fields.any(
|
||||
f => !Em.isBlank(f.value) && f.type_id !== SETTINGS_TYPE_ID
|
||||
);
|
||||
},
|
||||
|
||||
@computed("model.theme_fields.@each")
|
||||
@ -72,36 +78,31 @@ export default Ember.Controller.extend({
|
||||
"model",
|
||||
"allowChildThemes"
|
||||
)
|
||||
selectableChildThemes(available, childThemes, model, allowChildThemes) {
|
||||
selectableChildThemes(available, childThemes, allowChildThemes) {
|
||||
if (!allowChildThemes && (!childThemes || childThemes.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let themes = [];
|
||||
available.forEach(t => {
|
||||
if (
|
||||
(!childThemes || childThemes.indexOf(t) === -1) &&
|
||||
Em.isEmpty(t.get("childThemes")) &&
|
||||
!t.get("user_selectable") &&
|
||||
!t.get("default")
|
||||
) {
|
||||
if (!childThemes || childThemes.indexOf(t) === -1) {
|
||||
themes.push(t);
|
||||
}
|
||||
});
|
||||
return themes.length === 0 ? null : themes;
|
||||
},
|
||||
|
||||
@computed("allThemes", "allThemes.length", "model", "parentThemes")
|
||||
availableChildThemes(allThemes, count) {
|
||||
if (count === 1 || this.get("parentThemes")) {
|
||||
@computed("allThemes", "allThemes.length", "model.component", "model")
|
||||
availableChildThemes(allThemes, count, component) {
|
||||
if (count === 1 || component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let excludeIds = [this.get("model.id")];
|
||||
const themeId = this.get("model.id");
|
||||
|
||||
let themes = [];
|
||||
allThemes.forEach(theme => {
|
||||
if (excludeIds.indexOf(theme.get("id")) === -1) {
|
||||
if (themeId !== theme.get("id") && theme.get("component")) {
|
||||
themes.push(theme);
|
||||
}
|
||||
});
|
||||
@ -109,6 +110,12 @@ export default Ember.Controller.extend({
|
||||
return themes;
|
||||
},
|
||||
|
||||
@computed("model.component")
|
||||
switchKey(component) {
|
||||
const type = component ? "component" : "theme";
|
||||
return `admin.customize.theme.switch_${type}`;
|
||||
},
|
||||
|
||||
@computed("model.settings")
|
||||
settings(settings) {
|
||||
return settings.map(setting => ThemeSettings.create(setting));
|
||||
@ -254,6 +261,33 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
switchType() {
|
||||
return bootbox.confirm(
|
||||
I18n.t(`${this.get("switchKey")}_alert`),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
result => {
|
||||
if (result) {
|
||||
const model = this.get("model");
|
||||
model.set("component", !model.get("component"));
|
||||
model
|
||||
.saveChanges("component")
|
||||
.then(() => {
|
||||
this.set("colorSchemeId", null);
|
||||
model.setProperties({
|
||||
default: false,
|
||||
color_scheme_id: null,
|
||||
user_selectable: false,
|
||||
child_themes: [],
|
||||
childThemes: []
|
||||
});
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
@computed("model", "model.@each")
|
||||
sortedThemes(themes) {
|
||||
return _.sortBy(themes.content, t => {
|
||||
@computed("model", "model.@each", "model.@each.component")
|
||||
fullThemes(themes) {
|
||||
return _.sortBy(themes.filter(t => !t.get("component")), t => {
|
||||
return [
|
||||
!t.get("default"),
|
||||
!t.get("user_selectable"),
|
||||
t.get("name").toLowerCase()
|
||||
];
|
||||
});
|
||||
},
|
||||
|
||||
@computed("model", "model.@each", "model.@each.component")
|
||||
childThemes(themes) {
|
||||
return themes.filter(t => t.get("component"));
|
||||
}
|
||||
});
|
||||
|
||||
@ -40,6 +40,14 @@ export default Ember.Controller.extend(PeriodComputationMixin, {
|
||||
];
|
||||
},
|
||||
|
||||
@computed
|
||||
activityMetricsFilters() {
|
||||
return {
|
||||
startDate: this.get("lastMonth"),
|
||||
endDate: this.get("today")
|
||||
};
|
||||
},
|
||||
|
||||
@computed
|
||||
trendingSearchOptions() {
|
||||
return { table: { total: false, limit: 8 } };
|
||||
|
||||
@ -28,30 +28,25 @@ export default Ember.Controller.extend({
|
||||
sentTestEmail: false
|
||||
});
|
||||
|
||||
var self = this;
|
||||
ajax("/admin/email/test", {
|
||||
type: "POST",
|
||||
data: { email_address: this.get("testEmailAddress") }
|
||||
})
|
||||
.then(
|
||||
function() {
|
||||
self.set("sentTestEmail", true);
|
||||
},
|
||||
function(e) {
|
||||
if (e.responseJSON && e.responseJSON.errors) {
|
||||
bootbox.alert(
|
||||
I18n.t("admin.email.error", {
|
||||
server_error: e.responseJSON.errors[0]
|
||||
})
|
||||
);
|
||||
} else {
|
||||
bootbox.alert(I18n.t("admin.email.test_error"));
|
||||
}
|
||||
}
|
||||
.then(response =>
|
||||
this.set("sentTestEmailMessage", response.send_test_email_message)
|
||||
)
|
||||
.finally(function() {
|
||||
self.set("sendingEmail", false);
|
||||
});
|
||||
.catch(e => {
|
||||
if (e.responseJSON && e.responseJSON.errors) {
|
||||
bootbox.alert(
|
||||
I18n.t("admin.email.error", {
|
||||
server_error: e.responseJSON.errors[0]
|
||||
})
|
||||
);
|
||||
} else {
|
||||
bootbox.alert(I18n.t("admin.email.test_error"));
|
||||
}
|
||||
})
|
||||
.finally(() => this.set("sendingEmail", false));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
|
||||
const COMPONENT = "component";
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
types: [
|
||||
{ name: I18n.t("admin.customize.theme.theme"), value: "theme" },
|
||||
{ name: I18n.t("admin.customize.theme.component"), value: COMPONENT }
|
||||
],
|
||||
selectedType: "theme",
|
||||
name: I18n.t("admin.customize.new_style"),
|
||||
themesController: Ember.inject.controller("adminCustomizeThemes"),
|
||||
loading: false,
|
||||
|
||||
@computed("selectedType")
|
||||
component(type) {
|
||||
return type === COMPONENT;
|
||||
},
|
||||
|
||||
actions: {
|
||||
createTheme() {
|
||||
this.set("loading", true);
|
||||
const theme = this.store.createRecord("theme");
|
||||
theme
|
||||
.save({ name: this.get("name"), component: this.get("component") })
|
||||
.then(() => {
|
||||
this.get("themesController").send("addTheme", theme);
|
||||
this.send("closeModal");
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => this.set("loading", false));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -42,6 +42,15 @@ export default Ember.Mixin.create({
|
||||
.subtract(1, "week");
|
||||
},
|
||||
|
||||
@computed()
|
||||
lastMonth() {
|
||||
return moment()
|
||||
.locale("en")
|
||||
.utc()
|
||||
.startOf("day")
|
||||
.subtract(1, "month");
|
||||
},
|
||||
|
||||
@computed()
|
||||
endDate() {
|
||||
return moment()
|
||||
@ -51,6 +60,14 @@ export default Ember.Mixin.create({
|
||||
.endOf("day");
|
||||
},
|
||||
|
||||
@computed()
|
||||
today() {
|
||||
return moment()
|
||||
.locale("en")
|
||||
.utc()
|
||||
.endOf("day");
|
||||
},
|
||||
|
||||
actions: {
|
||||
changePeriod(period) {
|
||||
DiscourseURL.routeTo(this._reportsForPeriodURL(period));
|
||||
|
||||
@ -8,7 +8,7 @@ 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 = 2;
|
||||
export const SCHEMA_VERSION = 3;
|
||||
|
||||
const Report = Discourse.Model.extend({
|
||||
average: false,
|
||||
@ -439,7 +439,7 @@ const Report = Discourse.Model.extend({
|
||||
case "high-trending-down":
|
||||
return higherIsBetter ? "angle-double-down" : "angle-double-up";
|
||||
default:
|
||||
return null;
|
||||
return "minus";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { scrollTop } from "discourse/mixins/scroll-top";
|
||||
|
||||
export default Ember.Route.extend({
|
||||
serialize(model) {
|
||||
return { theme_id: model.get("id") };
|
||||
@ -10,14 +12,37 @@ export default Ember.Route.extend({
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
this._super(...arguments);
|
||||
|
||||
controller.set("model", model);
|
||||
|
||||
const parentController = this.controllerFor("adminCustomizeThemes");
|
||||
parentController.set("editingTheme", false);
|
||||
controller.set("allThemes", parentController.get("model"));
|
||||
|
||||
this.handleHighlight(model);
|
||||
|
||||
controller.set(
|
||||
"colorSchemes",
|
||||
parentController.get("model.extras.color_schemes")
|
||||
);
|
||||
controller.set("colorSchemeId", model.get("color_scheme_id"));
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this.handleHighlight();
|
||||
},
|
||||
|
||||
handleHighlight(theme) {
|
||||
this.get("controller.allThemes").forEach(t => t.set("active", false));
|
||||
if (theme) {
|
||||
theme.set("active", true);
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
didTransition() {
|
||||
scrollTop();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
|
||||
export default Ember.Route.extend({
|
||||
model() {
|
||||
@ -22,16 +21,8 @@ export default Ember.Route.extend({
|
||||
this.transitionTo("adminCustomizeThemes.show", theme.get("id"));
|
||||
},
|
||||
|
||||
newTheme(obj) {
|
||||
obj = obj || { name: I18n.t("admin.customize.new_style") };
|
||||
const item = this.store.createRecord("theme");
|
||||
|
||||
item
|
||||
.save(obj)
|
||||
.then(() => {
|
||||
this.send("addTheme", item);
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
showCreateModal() {
|
||||
showModal("admin-create-theme", { admin: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -16,5 +16,9 @@
|
||||
</div>
|
||||
|
||||
<div class="cell value thirty-days-count {{model.thirtyDaysTrend}}" title={{model.thirtyDaysCountTitle}}>
|
||||
{{number model.lastThirtyDaysCount}} {{d-icon model.thirtyDaysTrendIcon}}
|
||||
{{number model.lastThirtyDaysCount}}
|
||||
|
||||
{{#if model.prev30Days}}
|
||||
{{d-icon model.thirtyDaysTrendIcon}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@ -57,21 +57,28 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="alert alert-info report-alert no-data">
|
||||
{{d-icon "pie-chart"}}
|
||||
{{#if model.reportUrl}}
|
||||
<a href="{{model.reportUrl}}" class="report-url">
|
||||
<span>
|
||||
{{#if model.title}}
|
||||
{{model.title}} —
|
||||
{{/if}}
|
||||
{{i18n "admin.dashboard.reports.no_data"}}
|
||||
</span>
|
||||
</a>
|
||||
{{else}}
|
||||
<span>{{i18n "admin.dashboard.reports.no_data"}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if rateLimitationString}}
|
||||
<div class="alert alert-error report-alert rate-limited">
|
||||
{{d-icon "thermometer-three-quarters"}}
|
||||
<span>{{rateLimitationString}}</span>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="alert alert-info report-alert no-data">
|
||||
{{d-icon "pie-chart"}}
|
||||
{{#if model.reportUrl}}
|
||||
<a href="{{model.reportUrl}}" class="report-url">
|
||||
<span>
|
||||
{{#if model.title}}
|
||||
{{model.title}} —
|
||||
{{/if}}
|
||||
{{i18n "admin.dashboard.reports.no_data"}}
|
||||
</span>
|
||||
</a>
|
||||
{{else}}
|
||||
<span>{{i18n "admin.dashboard.reports.no_data"}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if showTimeoutError}}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
<div class="themes-list-header">
|
||||
<b>{{I18n title}}</b>
|
||||
</div>
|
||||
|
||||
<div class="themes-list-container">
|
||||
{{#if hasThemes}}
|
||||
{{#each themes as |theme|}}
|
||||
<div class="themes-list-item {{if theme.active 'active' ''}}">
|
||||
{{#link-to 'adminCustomizeThemes.show' theme replace=true}}
|
||||
{{plugin-outlet name="admin-customize-themes-list-item" connectorTagName='span' args=(hash theme=theme)}}
|
||||
{{theme.name}}
|
||||
{{#if theme.user_selectable}}
|
||||
{{d-icon "user"}}
|
||||
{{/if}}
|
||||
{{#if theme.default}}
|
||||
{{d-icon "asterisk"}}
|
||||
{{/if}}
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<div class="themes-list-item">
|
||||
<span class="empty">{{I18n "admin.customize.theme.empty"}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
@ -31,14 +31,6 @@
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if showSettings}}
|
||||
<li class='theme-settings'>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'settings' fieldName replace=true}}
|
||||
{{i18n 'admin.customize.theme.settings'}}
|
||||
{{d-icon 'cog'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
<div class='show-overidden'>
|
||||
<label>
|
||||
|
||||
@ -22,32 +22,34 @@
|
||||
|
||||
|
||||
{{#if parentThemes}}
|
||||
<h3>{{i18n "admin.customize.theme.component_of"}}</h3>
|
||||
<ul>
|
||||
{{#each parentThemes as |theme|}}
|
||||
<li>{{#link-to 'adminCustomizeThemes.show' theme replace=true}}{{theme.name}}{{/link-to}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p>
|
||||
{{inline-edit-checkbox action="applyDefault" labelKey="admin.customize.theme.is_default" checked=model.default}}
|
||||
{{inline-edit-checkbox action="applyUserSelectable" labelKey="admin.customize.theme.user_selectable" checked=model.user_selectable}}
|
||||
</p>
|
||||
|
||||
<h3>{{i18n "admin.customize.theme.color_scheme"}}</h3>
|
||||
<p>{{i18n "admin.customize.theme.color_scheme_select"}}</p>
|
||||
<p>{{combo-box content=colorSchemes
|
||||
filterable=true
|
||||
value=colorSchemeId
|
||||
icon="paint-brush"}}
|
||||
{{#if colorSchemeChanged}}
|
||||
{{d-button action="changeScheme" class="btn-primary btn-small submit-edit" icon="check"}}
|
||||
{{d-button action="cancelChangeScheme" class="btn-small cancel-edit" icon="times"}}
|
||||
{{/if}}
|
||||
</p>
|
||||
{{#link-to 'adminCustomize.colors' class="btn edit"}}{{i18n 'admin.customize.colors.edit'}}{{/link-to}}
|
||||
<h3>{{i18n "admin.customize.theme.component_of"}}</h3>
|
||||
<ul>
|
||||
{{#each parentThemes as |theme|}}
|
||||
<li>{{#link-to 'adminCustomizeThemes.show' theme replace=true}}{{theme.name}}{{/link-to}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
{{#unless model.component}}
|
||||
<p>
|
||||
{{inline-edit-checkbox action="applyDefault" labelKey="admin.customize.theme.is_default" checked=model.default}}
|
||||
{{inline-edit-checkbox action="applyUserSelectable" labelKey="admin.customize.theme.user_selectable" checked=model.user_selectable}}
|
||||
</p>
|
||||
|
||||
<h3>{{i18n "admin.customize.theme.color_scheme"}}</h3>
|
||||
<p>{{i18n "admin.customize.theme.color_scheme_select"}}</p>
|
||||
<p>{{combo-box content=colorSchemes
|
||||
filterable=true
|
||||
value=colorSchemeId
|
||||
icon="paint-brush"}}
|
||||
{{#if colorSchemeChanged}}
|
||||
{{d-button action="changeScheme" class="btn-primary btn-small submit-edit" icon="check"}}
|
||||
{{d-button action="cancelChangeScheme" class="btn-small cancel-edit" icon="times"}}
|
||||
{{/if}}
|
||||
</p>
|
||||
{{#link-to 'adminCustomize.colors' class="btn edit"}}{{i18n 'admin.customize.colors.edit'}}{{/link-to}}
|
||||
{{/unless}}
|
||||
|
||||
<h3>{{i18n "admin.customize.theme.css_html"}}</h3>
|
||||
{{#if hasEditedFields}}
|
||||
<p>{{i18n "admin.customize.theme.custom_sections"}}</p>
|
||||
@ -61,15 +63,18 @@
|
||||
{{i18n "admin.customize.theme.edit_css_html_help"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
<p>
|
||||
{{#if model.remote_theme}}
|
||||
{{#if model.remote_theme.commits_behind}}
|
||||
{{#d-button action="updateToLatest" icon="download" class='btn-primary'}}{{i18n "admin.customize.theme.update_to_latest"}}{{/d-button}}
|
||||
{{#d-button action="updateToLatest" icon="download" class='btn-primary'}}{{i18n "admin.customize.theme.update_to_latest"}}{{/d-button}}
|
||||
{{else}}
|
||||
{{#d-button action="checkForThemeUpdates" icon="refresh"}}{{i18n "admin.customize.theme.check_for_updates"}}{{/d-button}}
|
||||
{{#d-button action="checkForThemeUpdates" icon="refresh"}}{{i18n "admin.customize.theme.check_for_updates"}}{{/d-button}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#d-button action="editTheme" class="btn edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}}
|
||||
|
||||
{{#if model.remote_theme}}
|
||||
<span class='status-message'>
|
||||
{{#if updatingRemote}}
|
||||
@ -146,5 +151,6 @@
|
||||
<a href='{{previewUrl}}' title="{{i18n 'admin.customize.explain_preview'}}" target='_blank' class='btn'>{{d-icon 'desktop'}}{{i18n 'admin.customize.theme.preview'}}</a>
|
||||
<a class="btn export" target="_blank" href={{downloadUrl}}>{{d-icon "download"}} {{i18n 'admin.export_json.button_text'}}</a>
|
||||
|
||||
{{d-button action="switchType" label=switchKey icon="arrows-h" class="btn-danger"}}
|
||||
{{d-button action="destroy" label="admin.customize.delete" icon="trash" class="btn-danger"}}
|
||||
</div>
|
||||
|
||||
@ -1,25 +1,15 @@
|
||||
{{#unless editingTheme}}
|
||||
<div class='content-list'>
|
||||
<h3>{{i18n 'admin.customize.theme.long_title'}}</h3>
|
||||
<ul>
|
||||
{{#each sortedThemes as |theme|}}
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.show' theme replace=true}}
|
||||
{{plugin-outlet name="admin-customize-themes-list-item" connectorTagName='span' args=(hash theme=theme)}}
|
||||
{{theme.name}}
|
||||
{{#if theme.user_selectable}}
|
||||
{{d-icon "user"}}
|
||||
{{/if}}
|
||||
{{#if theme.default}}
|
||||
{{d-icon "asterisk"}}
|
||||
{{/if}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
{{d-button label="admin.customize.new" icon="plus" action="newTheme" class="btn-primary"}}
|
||||
{{d-button action="importModal" icon="upload" label="admin.customize.import"}}
|
||||
<div class="create-actions">
|
||||
{{d-button label="admin.customize.new" icon="plus" action="showCreateModal" class="btn-primary"}}
|
||||
{{d-button action="importModal" icon="upload" label="admin.customize.import"}}
|
||||
</div>
|
||||
|
||||
{{themes-list themes=fullThemes title="admin.customize.theme.title"}}
|
||||
{{themes-list themes=childThemes title="admin.customize.theme.components"}}
|
||||
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{outlet}}
|
||||
|
||||
@ -79,6 +79,7 @@
|
||||
{{#each activityMetrics as |metric|}}
|
||||
{{admin-report
|
||||
showHeader=false
|
||||
filters=activityMetricsFilters
|
||||
forcedModes="counters"
|
||||
dataSourceName=metric}}
|
||||
{{/each}}
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
</div>
|
||||
<div class='controls'>
|
||||
<button class='btn btn-primary' {{action "sendTestEmail"}} disabled={{sendTestEmailDisabled}}>{{i18n 'admin.email.send_test'}}</button>
|
||||
{{#if sentTestEmail}}<span class='result-message'>{{i18n 'admin.email.sent_test'}}</span>{{/if}}
|
||||
{{#if sentTestEmailMessage}}<span class='result-message'>{{sentTestEmailMessage}}</span>{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
{{#d-modal-body class="create-theme-modal" title="admin.customize.theme.modal_title"}}
|
||||
<div class="input">
|
||||
<span class="label">
|
||||
{{I18n "admin.customize.theme.create_name"}}
|
||||
</span>
|
||||
<span class="control">
|
||||
{{input value=name}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="input">
|
||||
<span class="label">
|
||||
{{I18n "admin.customize.theme.create_type"}}
|
||||
</span>
|
||||
<span class="control">
|
||||
{{combo-box valueAttribute="value" content=types value=selectedType}}
|
||||
</span>
|
||||
</div>
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button class="btn btn-primary" label="admin.customize.theme.create" action="createTheme" disabled=loading}}
|
||||
{{d-modal-cancel close=(action "closeModal")}}
|
||||
</div>
|
||||
@ -38,6 +38,7 @@ export default Em.Component.extend({
|
||||
{ name: I18n.t("search.advanced.filters.tracking"), value: "tracking" },
|
||||
{ name: I18n.t("search.advanced.filters.bookmarks"), value: "bookmarks" }
|
||||
],
|
||||
|
||||
inOptionsForAll: [
|
||||
{ name: I18n.t("search.advanced.filters.first"), value: "first" },
|
||||
{ name: I18n.t("search.advanced.filters.pinned"), value: "pinned" },
|
||||
@ -45,6 +46,7 @@ export default Em.Component.extend({
|
||||
{ name: I18n.t("search.advanced.filters.wiki"), value: "wiki" },
|
||||
{ name: I18n.t("search.advanced.filters.images"), value: "images" }
|
||||
],
|
||||
|
||||
statusOptions: [
|
||||
{ name: I18n.t("search.advanced.statuses.open"), value: "open" },
|
||||
{ name: I18n.t("search.advanced.statuses.closed"), value: "closed" },
|
||||
@ -55,6 +57,7 @@ export default Em.Component.extend({
|
||||
value: "single_user"
|
||||
}
|
||||
],
|
||||
|
||||
postTimeOptions: [
|
||||
{ name: I18n.t("search.advanced.post.time.before"), value: "before" },
|
||||
{ name: I18n.t("search.advanced.post.time.after"), value: "after" }
|
||||
@ -116,29 +119,36 @@ export default Em.Component.extend({
|
||||
this.setSearchedTermValueForGroup();
|
||||
this.setSearchedTermValueForBadge();
|
||||
this.setSearchedTermValueForTags();
|
||||
|
||||
this.setSearchedTermValue(
|
||||
"searchedTerms.in",
|
||||
REGEXP_IN_PREFIX,
|
||||
REGEXP_IN_MATCH
|
||||
);
|
||||
|
||||
this.setSearchedTermSpecialInValue(
|
||||
"searchedTerms.special.in.likes",
|
||||
REGEXP_SPECIAL_IN_LIKES_MATCH
|
||||
);
|
||||
|
||||
this.setSearchedTermSpecialInValue(
|
||||
"searchedTerms.special.in.title",
|
||||
REGEXP_SPECIAL_IN_TITLE_MATCH
|
||||
);
|
||||
|
||||
this.setSearchedTermSpecialInValue(
|
||||
"searchedTerms.special.in.private",
|
||||
REGEXP_SPECIAL_IN_PRIVATE_MATCH
|
||||
);
|
||||
|
||||
this.setSearchedTermSpecialInValue(
|
||||
"searchedTerms.special.in.seen",
|
||||
REGEXP_SPECIAL_IN_SEEN_MATCH
|
||||
);
|
||||
|
||||
this.setSearchedTermValue("searchedTerms.status", REGEXP_STATUS_PREFIX);
|
||||
this.setSearchedTermValueForPostTime();
|
||||
|
||||
this.setSearchedTermValue(
|
||||
"searchedTerms.min_post_count",
|
||||
REGEXP_MIN_POST_COUNT_PREFIX
|
||||
@ -304,14 +314,17 @@ export default Em.Component.extend({
|
||||
const userInputWhen = match[0].match(REGEXP_POST_TIME_WHEN)[0];
|
||||
const existingInputDays = this.get("searchedTerms.time.days");
|
||||
const userInputDays = match[0].replace(REGEXP_POST_TIME_PREFIX, "");
|
||||
const properties = {};
|
||||
|
||||
if (existingInputWhen !== userInputWhen) {
|
||||
this.set("searchedTerms.time.when", userInputWhen);
|
||||
properties["searchedTerms.time.when"] = userInputWhen;
|
||||
}
|
||||
|
||||
if (existingInputDays !== userInputDays) {
|
||||
this.set("searchedTerms.time.days", userInputDays);
|
||||
properties["searchedTerms.time.days"] = userInputDays;
|
||||
}
|
||||
|
||||
this.setProperties(properties);
|
||||
} else {
|
||||
this.set("searchedTerms.time.days", "");
|
||||
}
|
||||
|
||||
@ -203,12 +203,17 @@ export default Ember.Controller.extend({
|
||||
|
||||
canUnlistTopic: Em.computed.and("model.creatingTopic", "isStaffUser"),
|
||||
|
||||
@computed("model.action", "isStaffUser")
|
||||
canWhisper(action, isStaffUser) {
|
||||
@computed("canWhisper", "model.whisper")
|
||||
showWhisperToggle(canWhisper, isWhisper) {
|
||||
return canWhisper && !isWhisper;
|
||||
},
|
||||
|
||||
@computed("isStaffUser", "model.action")
|
||||
canWhisper(isStaffUser, action) {
|
||||
return (
|
||||
isStaffUser &&
|
||||
this.siteSettings.enable_whispers &&
|
||||
action === Composer.REPLY
|
||||
isStaffUser &&
|
||||
Composer.REPLY === action
|
||||
);
|
||||
},
|
||||
|
||||
@ -246,7 +251,7 @@ export default Ember.Controller.extend({
|
||||
action: "toggleWhisper",
|
||||
icon: "eye-slash",
|
||||
label: "composer.toggle_whisper",
|
||||
condition: "canWhisper"
|
||||
condition: "showWhisperToggle"
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@ -28,6 +28,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
loggedIn: false,
|
||||
processingEmailLink: false,
|
||||
showLoginButtons: true,
|
||||
showSecondFactor: false,
|
||||
|
||||
canLoginLocal: setting("enable_local_logins"),
|
||||
canLoginLocalWithEmail: setting("enable_local_logins_via_email"),
|
||||
@ -40,10 +41,19 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
loggingIn: false,
|
||||
loggedIn: false,
|
||||
secondFactorRequired: false,
|
||||
showSecondFactor: false,
|
||||
showLoginButtons: true
|
||||
});
|
||||
$("#credentials").show();
|
||||
$("#second-factor").hide();
|
||||
},
|
||||
|
||||
@computed("showSecondFactor")
|
||||
credentialsClass(showSecondFactor) {
|
||||
return showSecondFactor ? "hidden" : "";
|
||||
},
|
||||
|
||||
@computed("showSecondFactor")
|
||||
secondFactorClass(showSecondFactor) {
|
||||
return showSecondFactor ? "" : "hidden";
|
||||
},
|
||||
|
||||
// Determines whether at least one login button is enabled
|
||||
@ -113,12 +123,13 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
self.setProperties({
|
||||
secondFactorRequired: true,
|
||||
showLoginButtons: false,
|
||||
backupEnabled: result.backup_enabled
|
||||
backupEnabled: result.backup_enabled,
|
||||
showSecondFactor: true
|
||||
});
|
||||
|
||||
$("#credentials").hide();
|
||||
$("#second-factor").show();
|
||||
$("#second-factor input").focus();
|
||||
Ember.run.next(() => {
|
||||
$("#second-factor input").focus();
|
||||
});
|
||||
|
||||
return;
|
||||
} else if (result.reason === "not_activated") {
|
||||
|
||||
@ -20,6 +20,16 @@ export default Ember.Controller.extend({
|
||||
),
|
||||
unchanged: propertyEqual("newEmailLower", "currentUser.email"),
|
||||
|
||||
reset: function() {
|
||||
this.setProperties({
|
||||
taken: false,
|
||||
saving: false,
|
||||
error: false,
|
||||
success: false,
|
||||
newEmail: null
|
||||
});
|
||||
},
|
||||
|
||||
newEmailLower: function() {
|
||||
return this.get("newEmail")
|
||||
.toLowerCase()
|
||||
|
||||
@ -98,6 +98,16 @@ export default Ember.Controller.extend(BufferedContent, {
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
this.appEvents.on("post:show-revision", (postNumber, revision) => {
|
||||
const post = this.model.get("postStream").postForPostNumber(postNumber);
|
||||
if (!post) {
|
||||
return;
|
||||
}
|
||||
|
||||
Ember.run.scheduleOnce("afterRender", () => {
|
||||
this.send("showHistory", post, revision);
|
||||
});
|
||||
});
|
||||
this.setProperties({
|
||||
selectedPostIds: [],
|
||||
quoteState: new QuoteState()
|
||||
|
||||
@ -61,6 +61,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
if (this.get("showMore") && imageLink.length > 3) {
|
||||
toolbarEvent.addText(`[](${imageLink})`);
|
||||
} else if (imageUrl.match(/\.(jpg|jpeg|png|gif)$/)) {
|
||||
toolbarEvent.addText(``);
|
||||
} else {
|
||||
toolbarEvent.addText(imageUrl);
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ function renderAvatar(user, options) {
|
||||
options = options || {};
|
||||
|
||||
if (user) {
|
||||
const name = Em.get(user, options.namePath || "name");
|
||||
const username = Em.get(user, options.usernamePath || "username");
|
||||
const avatarTemplate = Em.get(
|
||||
user,
|
||||
@ -40,7 +41,7 @@ function renderAvatar(user, options) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let displayName = Ember.get(user, "name") || formatUsername(username);
|
||||
let displayName = name || formatUsername(username);
|
||||
|
||||
let title = options.title;
|
||||
if (!title && !options.ignoreTitle) {
|
||||
|
||||
@ -297,10 +297,14 @@ export default {
|
||||
sendToSelectedPost(action) {
|
||||
const container = this.container;
|
||||
// TODO: We should keep track of the post without a CSS class
|
||||
const selectedPostId = parseInt(
|
||||
let selectedPostId = parseInt(
|
||||
$(".topic-post.selected article.boxed").data("post-id"),
|
||||
10
|
||||
);
|
||||
if (!selectedPostId) {
|
||||
// If no post was selected, automatically select the hovered post.
|
||||
selectedPostId = parseInt($("article.boxed:hover").data("post-id"), 10);
|
||||
}
|
||||
if (selectedPostId) {
|
||||
const topicController = container.lookup("controller:topic");
|
||||
const post = topicController
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export default function renderTag(tag, params) {
|
||||
params = params || {};
|
||||
tag = Handlebars.Utils.escapeExpression(tag);
|
||||
const classes = ["tag-" + tag, "discourse-tag"];
|
||||
const classes = ["discourse-tag"];
|
||||
const tagName = params.tagName || "a";
|
||||
let path;
|
||||
if (tagName === "a" && !params.noHref) {
|
||||
@ -24,6 +24,8 @@ export default function renderTag(tag, params) {
|
||||
"<" +
|
||||
tagName +
|
||||
href +
|
||||
" data-tag-name=" +
|
||||
tag +
|
||||
" class='" +
|
||||
classes.join(" ") +
|
||||
"'>" +
|
||||
|
||||
87
app/assets/javascripts/discourse/lib/reports-loader.js.es6
Normal file
87
app/assets/javascripts/discourse/lib/reports-loader.js.es6
Normal file
@ -0,0 +1,87 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
const { debounce } = Ember.run;
|
||||
|
||||
let _queue = [];
|
||||
let _processing = 0;
|
||||
|
||||
// max number of reports which will be requested in one bulk request
|
||||
const MAX_JOB_SIZE = 5;
|
||||
|
||||
// max number of concurrent bulk requests
|
||||
const MAX_CONCURRENCY = 3;
|
||||
|
||||
// max number of jobs stored, first entered jobs will be evicted first
|
||||
const MAX_QUEUE_SIZE = 20;
|
||||
|
||||
const BULK_REPORTS_ENDPOINT = "/admin/reports/bulk";
|
||||
|
||||
const DEBOUNCING_DELAY = 50;
|
||||
|
||||
export default {
|
||||
enqueue(type, params, callback) {
|
||||
// makes sures the queue is not filling indefinitely
|
||||
if (_queue.length >= MAX_QUEUE_SIZE) {
|
||||
const removedJobs = _queue.splice(0, 1)[0];
|
||||
removedJobs.forEach(job => {
|
||||
// this is technically not a 429, but it's the result
|
||||
// of client doing too many requests so we want the same
|
||||
// behavior
|
||||
job.runnable()(429);
|
||||
});
|
||||
}
|
||||
|
||||
_queue.push({ runnable: () => callback, type, params });
|
||||
|
||||
debounce(this, this._processQueue, DEBOUNCING_DELAY);
|
||||
},
|
||||
|
||||
_processQueue() {
|
||||
if (_queue.length === 0) return;
|
||||
if (_processing >= MAX_CONCURRENCY) return;
|
||||
|
||||
_processing++;
|
||||
|
||||
const jobs = _queue.splice(0, MAX_JOB_SIZE);
|
||||
|
||||
// if queue has still jobs after splice, we request a future processing
|
||||
if (_queue.length > 0) {
|
||||
debounce(this, this._processQueue, DEBOUNCING_DELAY);
|
||||
}
|
||||
|
||||
let reports = {};
|
||||
jobs.forEach(job => {
|
||||
reports[job.type] = job.params;
|
||||
});
|
||||
|
||||
ajax(BULK_REPORTS_ENDPOINT, { data: { reports } })
|
||||
.then(response => {
|
||||
jobs.forEach(job => {
|
||||
const report = response.reports.findBy("type", job.type);
|
||||
job.runnable()(report);
|
||||
});
|
||||
})
|
||||
.catch(data => {
|
||||
jobs.forEach(job => {
|
||||
if (data.jqXHR && data.jqXHR.status === 429) {
|
||||
job.runnable()(429);
|
||||
} else if (data.jqXHR && data.jqXHR.status === 500) {
|
||||
job.runnable()(500);
|
||||
} else {
|
||||
job.runnable()();
|
||||
}
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
_processing--;
|
||||
|
||||
// when a request is done we want to start processing queue
|
||||
// without waiting for debouncing
|
||||
debounce(this, this._processQueue, DEBOUNCING_DELAY, true);
|
||||
});
|
||||
},
|
||||
|
||||
_reset() {
|
||||
_queue = [];
|
||||
_processing = 0;
|
||||
}
|
||||
};
|
||||
@ -109,6 +109,13 @@ export class Tag {
|
||||
}
|
||||
|
||||
decorate(text) {
|
||||
const parent = this.element.parent;
|
||||
|
||||
if (this.name === "p" && parent && parent.name === "li") {
|
||||
// fix for google docs
|
||||
this.gap = "";
|
||||
}
|
||||
|
||||
return `${this.gap}${this.prefix}${text}${this.suffix}${this.gap}`;
|
||||
}
|
||||
};
|
||||
@ -162,6 +169,24 @@ export class Tag {
|
||||
};
|
||||
}
|
||||
|
||||
static span() {
|
||||
return class extends Tag {
|
||||
constructor() {
|
||||
super("span");
|
||||
}
|
||||
|
||||
decorate(text) {
|
||||
const attr = this.element.attributes;
|
||||
|
||||
if (attr.class === "badge badge-notification clicks") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return super.decorate(text);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static link() {
|
||||
return class extends Tag {
|
||||
constructor() {
|
||||
@ -200,6 +225,11 @@ export class Tag {
|
||||
const attr = e.attributes;
|
||||
const pAttr = (e.parent && e.parent.attributes) || {};
|
||||
const src = attr.src || pAttr.src;
|
||||
const cssClass = attr.class || pAttr.class;
|
||||
|
||||
if (cssClass === "emoji") {
|
||||
return attr.title || pAttr.title;
|
||||
}
|
||||
|
||||
if (src) {
|
||||
let alt = attr.alt || pAttr.alt || "";
|
||||
@ -377,6 +407,12 @@ export class Tag {
|
||||
return class extends Tag.block(name) {
|
||||
decorate(text) {
|
||||
let smallGap = "";
|
||||
const parent = this.element.parent;
|
||||
|
||||
if (parent && parent.name === "ul") {
|
||||
this.gap = "";
|
||||
this.suffix = "\n";
|
||||
}
|
||||
|
||||
if (this.element.filterParentNames(["li"]).length) {
|
||||
this.gap = "";
|
||||
@ -443,7 +479,8 @@ function tags() {
|
||||
Tag.table(),
|
||||
Tag.tr(),
|
||||
Tag.ol(),
|
||||
Tag.list("ul")
|
||||
Tag.list("ul"),
|
||||
Tag.span()
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -242,6 +242,10 @@ const DiscourseURL = Ember.Object.extend({
|
||||
|
||||
path = rewritePath(path);
|
||||
|
||||
if (typeof opts.afterRouteComplete === "function") {
|
||||
Ember.run.schedule("afterRender", opts.afterRouteComplete);
|
||||
}
|
||||
|
||||
if (this.navigatedToPost(oldPath, path, opts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -140,13 +140,6 @@ export function selectedText() {
|
||||
$div.append(range.cloneContents());
|
||||
}
|
||||
|
||||
// strip click counters
|
||||
$div.find(".clicks").remove();
|
||||
// replace emojis
|
||||
$div.find("img.emoji").replaceWith(function() {
|
||||
return this.title;
|
||||
});
|
||||
|
||||
return toMarkdown($div.html());
|
||||
}
|
||||
|
||||
@ -441,8 +434,9 @@ export function uploadLocation(url) {
|
||||
export function getUploadMarkdown(upload) {
|
||||
if (isAnImage(upload.original_filename)) {
|
||||
const name = imageNameFromFileName(upload.original_filename);
|
||||
return ``;
|
||||
return ``;
|
||||
} else if (
|
||||
!Discourse.SiteSettings.prevent_anons_from_downloading_files &&
|
||||
/\.(mov|mp4|webm|ogv|mp3|ogg|wav|m4a)$/i.test(upload.original_filename)
|
||||
|
||||
@ -711,6 +711,16 @@ export default RestModel.extend({
|
||||
return resolved;
|
||||
},
|
||||
|
||||
postForPostNumber(postNumber) {
|
||||
if (!this.get("hasPosts")) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.get("posts").find(p => {
|
||||
return p.get("post_number") === postNumber;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Returns the closest post given a postNumber that may not exist in the stream.
|
||||
For example, if the user asks for a post that's deleted or otherwise outside the range.
|
||||
|
||||
@ -179,6 +179,7 @@ export default function() {
|
||||
this.route("tos", { path: "/tos" });
|
||||
this.route("privacy", { path: "/privacy" });
|
||||
this.route("guidelines", { path: "/guidelines" });
|
||||
this.route("rules", { path: "/rules" });
|
||||
|
||||
this.route("new-topic", { path: "/new-topic" });
|
||||
this.route("new-message", { path: "/new-message" });
|
||||
|
||||
@ -119,7 +119,7 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
|
||||
everyoneName = groups.findBy("id", 0).name;
|
||||
|
||||
const model = this.store.createRecord("category", {
|
||||
color: "AB9364",
|
||||
color: "0088CC",
|
||||
text_color: "FFFFFF",
|
||||
group_permissions: [{ group_name: everyoneName, permission_type: 1 }],
|
||||
available_groups: groups.map(g => g.name),
|
||||
|
||||
@ -12,6 +12,7 @@ export default RestrictedUserRoute.extend({
|
||||
},
|
||||
|
||||
setupController: function(controller, model) {
|
||||
controller.reset();
|
||||
controller.setProperties({ model: model, newEmail: model.get("email") });
|
||||
},
|
||||
|
||||
|
||||
3
app/assets/javascripts/discourse/routes/rules.js.es6
Normal file
3
app/assets/javascripts/discourse/routes/rules.js.es6
Normal file
@ -0,0 +1,3 @@
|
||||
import staticRouteBuilder from "discourse/lib/static-route-builder";
|
||||
|
||||
export default staticRouteBuilder("rules");
|
||||
@ -184,9 +184,11 @@ export default Discourse.Route.extend({
|
||||
var c = self.controllerFor("composer").get("model");
|
||||
c.set(
|
||||
"tags",
|
||||
_.flatten(
|
||||
[controller.get("model.id")],
|
||||
controller.get("additionalTags")
|
||||
_.compact(
|
||||
_.flatten([
|
||||
controller.get("model.id"),
|
||||
controller.get("additionalTags")
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -91,11 +91,11 @@ const TopicRoute = Discourse.Route.extend({
|
||||
this.controllerFor("invite").reset();
|
||||
},
|
||||
|
||||
showHistory(model) {
|
||||
showHistory(model, revision) {
|
||||
showModal("history", { model });
|
||||
const historyController = this.controllerFor("history");
|
||||
|
||||
historyController.refresh(model.get("id"), "latest");
|
||||
historyController.refresh(model.get("id"), revision || "latest");
|
||||
historyController.set("post", model);
|
||||
historyController.set("topicController", this.controllerFor("topic"));
|
||||
|
||||
|
||||
@ -6,7 +6,11 @@
|
||||
{{#if show}}
|
||||
<div class="location-box">
|
||||
<a class="close pull-right" {{action "hide"}}>{{d-icon "times"}}</a>
|
||||
<a class="btn pull-right" {{action "copy"}}>{{d-icon "copy"}}</a>
|
||||
{{#if copied}}
|
||||
<a class="btn btn-hover pull-right">{{d-icon "copy"}} {{i18n "ip_lookup.copied"}}</a>
|
||||
{{else}}
|
||||
<a class="btn pull-right" {{action "copy"}}>{{d-icon "copy"}}</a>
|
||||
{{/if}}
|
||||
<h4>{{i18n 'ip_lookup.title'}}</h4>
|
||||
<p class='powered-by'>{{{i18n 'ip_lookup.powered_by'}}}</p>
|
||||
<dl>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<td class='posters'>
|
||||
{{#each posters as |poster|}}
|
||||
<a href="{{poster.user.path}}" data-user-card="{{poster.user.username}}" class="{{poster.extraClasses}}">{{avatar poster avatarTemplatePath="user.avatar_template" usernamePath="user.username" imageSize="small"}}</a>
|
||||
<a href="{{poster.user.path}}" data-user-card="{{poster.user.username}}" class="{{poster.extraClasses}}">{{avatar poster avatarTemplatePath="user.avatar_template" usernamePath="user.username" namePath="user.name" imageSize="small"}}</a>
|
||||
{{/each}}
|
||||
</td>
|
||||
|
||||
@ -10,14 +10,14 @@
|
||||
|
||||
{{#if canLoginLocal}}
|
||||
<form id='login-form' method='post'>
|
||||
<div id="credentials">
|
||||
<div id="credentials" class={{credentialsClass}}>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<label for='login-account-name'>{{i18n 'login.username'}}</label>
|
||||
</td>
|
||||
<td>
|
||||
{{text-field value=loginName type="email" placeholderKey="login.email_placeholder" id="login-account-name" autocorrect="off" autocapitalize="off"}}
|
||||
{{text-field value=loginName type="email" placeholderKey="login.email_placeholder" id="login-account-name" autocorrect="off" autocapitalize="off" disabled=showSecondFactor}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -25,7 +25,7 @@
|
||||
<label for='login-account-password'>{{i18n 'login.password'}}</label>
|
||||
</td>
|
||||
<td>
|
||||
{{text-field value=loginPassword type="password" id="login-account-password" maxlength="200"}}
|
||||
{{text-field value=loginPassword type="password" id="login-account-password" maxlength="200" disabled=showSecondFactor}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -36,7 +36,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{{#second-factor-form secondFactorMethod=secondFactorMethod loginSecondFactor=loginSecondFactor backupEnabled=backupEnabled}}
|
||||
{{#second-factor-form secondFactorMethod=secondFactorMethod loginSecondFactor=loginSecondFactor backupEnabled=backupEnabled class=secondFactorClass}}
|
||||
{{second-factor-input value=loginSecondFactor inputId='login-second-factor' secondFactorMethod=secondFactorMethod}}
|
||||
{{/second-factor-form}}
|
||||
</form>
|
||||
|
||||
@ -3,15 +3,15 @@
|
||||
|
||||
{{#if canLoginLocal}}
|
||||
<form id='login-form' method='post'>
|
||||
<div id="credentials">
|
||||
<div id="credentials" class={{credentialsClass}}>
|
||||
<table>
|
||||
<tr>
|
||||
<td><label for='login-account-name'>{{i18n 'login.username'}}</label></td>
|
||||
<td>{{text-field value=loginName placeholderKey="login.email_placeholder" id="login-account-name" autocorrect="off" autocapitalize="off" autofocus="autofocus"}}</td>
|
||||
<td>{{text-field value=loginName placeholderKey="login.email_placeholder" id="login-account-name" autocorrect="off" autocapitalize="off" autofocus="autofocus" disabled=showSecondFactor}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for='login-account-password'>{{i18n 'login.password'}}</label></td>
|
||||
<td>{{password-field value=loginPassword type="password" id="login-account-password" maxlength="200" capsLockOn=capsLockOn}}</td>
|
||||
<td>{{password-field value=loginPassword type="password" id="login-account-password" maxlength="200" capsLockOn=capsLockOn disabled=showSecondFactor}}</td>
|
||||
<td><a id="forgot-password-link" {{action "forgotPassword"}}>{{i18n 'forgot_password.action'}}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -21,7 +21,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{{#second-factor-form secondFactorMethod=secondFactorMethod loginSecondFactor=loginSecondFactor backupEnabled=backupEnabled}}
|
||||
{{#second-factor-form secondFactorMethod=secondFactorMethod loginSecondFactor=loginSecondFactor backupEnabled=backupEnabled class=secondFactorClass}}
|
||||
{{second-factor-input value=loginSecondFactor inputId='login-second-factor' secondFactorMethod=secondFactorMethod}}
|
||||
{{/second-factor-form}}
|
||||
</form>
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
|
||||
<div class="clearfix list-actions">
|
||||
{{#if showToggleBulkSelect}}
|
||||
<button {{action "toggleBulkSelect"}} class="btn bulk-select" title="{{i18n "user.messages.bulk_select"}}">
|
||||
<button {{action "toggleBulkSelect"}} class="btn bulk-select no-text" title="{{i18n "user.messages.bulk_select"}}">
|
||||
{{d-icon "list"}}
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
@ -155,10 +155,7 @@ export default createWidget("hamburger-menu", {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
this.siteSettings.enable_group_directory ||
|
||||
(this.currentUser && this.currentUser.staff)
|
||||
) {
|
||||
if (this.siteSettings.enable_group_directory) {
|
||||
links.push({
|
||||
route: "groups",
|
||||
className: "groups-link",
|
||||
|
||||
@ -52,6 +52,7 @@ createWidget("notification-item", {
|
||||
}
|
||||
|
||||
const topicId = attrs.topic_id;
|
||||
|
||||
if (topicId) {
|
||||
return postUrl(attrs.slug, topicId, attrs.post_number);
|
||||
}
|
||||
@ -152,6 +153,18 @@ createWidget("notification-item", {
|
||||
e.preventDefault();
|
||||
|
||||
this.sendWidgetEvent("linkClicked");
|
||||
DiscourseURL.routeTo(this.url());
|
||||
DiscourseURL.routeTo(this.url(), {
|
||||
afterRouteComplete: () => {
|
||||
if (!this.attrs.data.revision_number) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.appEvents.trigger(
|
||||
"post:show-revision",
|
||||
this.attrs.post_number,
|
||||
this.attrs.data.revision_number
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import { h } from "virtual-dom";
|
||||
import { avatarFor } from "discourse/widgets/post";
|
||||
import { avatarFor, avatarImg } from "discourse/widgets/post";
|
||||
import hbs from "discourse/widgets/hbs-compiler";
|
||||
|
||||
createWidget("pm-remove-group-link", {
|
||||
tagName: "a.remove-invited",
|
||||
tagName: "a.remove-invited.no-text.btn-icon.btn",
|
||||
template: hbs`{{d-icon "times"}}`,
|
||||
|
||||
click() {
|
||||
@ -30,16 +29,20 @@ createWidget("pm-map-user-group", {
|
||||
},
|
||||
|
||||
template: hbs`
|
||||
{{fa-icon 'users'}}
|
||||
<a href={{transformed.href}}>{{attrs.group.name}}</a>
|
||||
<a href={{transformed.href}} class="group-link">
|
||||
{{d-icon "users"}}
|
||||
<span class="group-name">{{attrs.group.name}}</span>
|
||||
</a>
|
||||
{{#if attrs.isEditing}}
|
||||
{{#if attrs.canRemoveAllowedUsers}}
|
||||
{{attach widget="pm-remove-group-link" attrs=attrs.group}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
`
|
||||
});
|
||||
|
||||
createWidget("pm-remove-link", {
|
||||
tagName: "a.remove-invited",
|
||||
tagName: "a.remove-invited.no-text.btn-icon.btn",
|
||||
template: hbs`{{d-icon "times"}}`,
|
||||
|
||||
click() {
|
||||
@ -65,20 +68,36 @@ createWidget("pm-map-user", {
|
||||
|
||||
html(attrs) {
|
||||
const user = attrs.user;
|
||||
const avatar = avatarFor("tiny", {
|
||||
template: user.avatar_template,
|
||||
username: user.username
|
||||
});
|
||||
const link = h("a", { attributes: { href: user.get("path") } }, [
|
||||
avatar,
|
||||
" ",
|
||||
h("span", user.username)
|
||||
]);
|
||||
const username = h("span.username", user.username);
|
||||
|
||||
let link;
|
||||
|
||||
if (this.site && this.site.mobileView) {
|
||||
const avatar = avatarImg("tiny", {
|
||||
template: user.avatar_template,
|
||||
username: user.username
|
||||
});
|
||||
link = h("a", { attributes: { href: user.get("path") } }, [
|
||||
avatar,
|
||||
username
|
||||
]);
|
||||
} else {
|
||||
const avatar = avatarFor("tiny", {
|
||||
template: user.avatar_template,
|
||||
username: user.username
|
||||
});
|
||||
|
||||
link = h(
|
||||
"a",
|
||||
{ attributes: { class: "user-link", href: user.get("path") } },
|
||||
[avatar, username]
|
||||
);
|
||||
}
|
||||
|
||||
const result = [link];
|
||||
const isCurrentUser = attrs.canRemoveSelfId === user.get("id");
|
||||
|
||||
if (attrs.canRemoveAllowedUsers || isCurrentUser) {
|
||||
result.push(" ");
|
||||
if (attrs.isEditing && (attrs.canRemoveAllowedUsers || isCurrentUser)) {
|
||||
result.push(this.attach("pm-remove-link", { user, isCurrentUser }));
|
||||
}
|
||||
|
||||
@ -89,6 +108,12 @@ createWidget("pm-map-user", {
|
||||
export default createWidget("private-message-map", {
|
||||
tagName: "section.information.private-message-map",
|
||||
|
||||
buildKey: attrs => `private-message-map-${attrs.id}`,
|
||||
|
||||
defaultState() {
|
||||
return { isEditing: false };
|
||||
},
|
||||
|
||||
html(attrs) {
|
||||
const participants = [];
|
||||
|
||||
@ -97,48 +122,61 @@ export default createWidget("private-message-map", {
|
||||
attrs.allowedGroups.map(group => {
|
||||
return this.attach("pm-map-user-group", {
|
||||
group,
|
||||
canRemoveAllowedUsers: attrs.canRemoveAllowedUsers
|
||||
canRemoveAllowedUsers: attrs.canRemoveAllowedUsers,
|
||||
isEditing: this.state.isEditing
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const allowedUsersLength = attrs.allowedUsers.length;
|
||||
|
||||
if (allowedUsersLength) {
|
||||
if (attrs.allowedUsers.length) {
|
||||
participants.push(
|
||||
attrs.allowedUsers.map(au => {
|
||||
return this.attach("pm-map-user", {
|
||||
user: au,
|
||||
canRemoveAllowedUsers: attrs.canRemoveAllowedUsers,
|
||||
canRemoveSelfId: attrs.canRemoveSelfId
|
||||
canRemoveSelfId: attrs.canRemoveSelfId,
|
||||
isEditing: this.state.isEditing
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const result = [
|
||||
h("h3", [
|
||||
iconNode("envelope"),
|
||||
" ",
|
||||
I18n.t("private_message_info.title")
|
||||
]),
|
||||
h("div.participants", participants)
|
||||
let hideNamesClass = "";
|
||||
if (
|
||||
!this.state.isEditing &&
|
||||
this.site.mobileView &&
|
||||
Ember.makeArray(participants[0]).length > 4
|
||||
) {
|
||||
hideNamesClass = ".hide-names";
|
||||
}
|
||||
|
||||
const result = [h(`div.participants${hideNamesClass}`, participants)];
|
||||
|
||||
const controls = [
|
||||
this.attach("button", {
|
||||
action: "toggleEditing",
|
||||
label: "private_message_info.edit",
|
||||
className: "btn add-remove-participant-btn"
|
||||
})
|
||||
];
|
||||
|
||||
if (attrs.canInvite) {
|
||||
result.push(
|
||||
h(
|
||||
"div.controls",
|
||||
this.attach("button", {
|
||||
action: "showInvite",
|
||||
label: "private_message_info.invite",
|
||||
className: "btn"
|
||||
})
|
||||
)
|
||||
if (attrs.canInvite && this.state.isEditing) {
|
||||
controls.push(
|
||||
this.attach("button", {
|
||||
action: "showInvite",
|
||||
icon: "plus",
|
||||
className: "btn.no-text.btn-icon"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
result.push(h("div.controls", controls));
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
toggleEditing() {
|
||||
this.state.isEditing = !this.state.isEditing;
|
||||
}
|
||||
});
|
||||
|
||||
@ -275,6 +275,15 @@ export default DropdownSelectBoxComponent.extend({
|
||||
if (postUsername) {
|
||||
usernames = postUsername;
|
||||
}
|
||||
} else if (this.get("composerModel.topic")) {
|
||||
const stream = this.get("composerModel.topic.postStream");
|
||||
|
||||
if (stream.get("firstPostPresent")) {
|
||||
const post = stream.get("posts.firstObject");
|
||||
if (post && !post.get("yours") && post.get("username")) {
|
||||
usernames = post.get("username");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
options.action = PRIVATE_MESSAGE;
|
||||
|
||||
@ -47,6 +47,8 @@
|
||||
|
||||
.header .trend {
|
||||
margin-left: auto;
|
||||
margin-right: 8px;
|
||||
|
||||
&.trending-down,
|
||||
&.high-trending-down {
|
||||
color: $danger;
|
||||
@ -59,6 +61,9 @@
|
||||
|
||||
&.no-change {
|
||||
color: $primary-medium;
|
||||
i {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
@ -96,12 +101,17 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.no-data {
|
||||
&.no-data,
|
||||
&.rate-limited {
|
||||
background: $secondary;
|
||||
border-color: $primary-low;
|
||||
color: $primary-low-mid;
|
||||
}
|
||||
|
||||
&.rate-limited .d-icon {
|
||||
color: $danger;
|
||||
}
|
||||
|
||||
&.timeout,
|
||||
&.exception {
|
||||
border-color: $danger-low;
|
||||
|
||||
@ -34,6 +34,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.d-icon-minus {
|
||||
color: $primary-medium;
|
||||
font-size: $font-down-3;
|
||||
}
|
||||
|
||||
&.high-trending-up,
|
||||
&.trending-up {
|
||||
i {
|
||||
|
||||
@ -44,6 +44,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.create-theme-modal {
|
||||
div.input {
|
||||
margin-bottom: 12px;
|
||||
.label {
|
||||
width: 20%;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-customize {
|
||||
h1 {
|
||||
margin-bottom: 10px;
|
||||
@ -101,6 +111,62 @@
|
||||
}
|
||||
}
|
||||
|
||||
.create-actions {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.themes-list {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.themes-list-header {
|
||||
font-size: $font-up-1;
|
||||
padding: 10px;
|
||||
background-color: $primary-low;
|
||||
}
|
||||
|
||||
.themes-list-container {
|
||||
max-height: 280px;
|
||||
overflow-y: scroll;
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
border-radius: 10px;
|
||||
background-color: $secondary;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
background-color: $primary-low-mid;
|
||||
}
|
||||
|
||||
.themes-list-item {
|
||||
color: $primary;
|
||||
border-bottom: 1px solid $primary-low;
|
||||
display: flex;
|
||||
|
||||
&:hover {
|
||||
background-color: $tertiary-low;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $secondary;
|
||||
font-weight: bold;
|
||||
background-color: $tertiary;
|
||||
}
|
||||
|
||||
a,
|
||||
span.empty {
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme.settings {
|
||||
.theme-setting {
|
||||
padding-bottom: 0;
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
|
||||
.navigation-item {
|
||||
display: inline;
|
||||
background: $secondary;
|
||||
&:hover {
|
||||
background: $primary-very-low;
|
||||
}
|
||||
@ -309,6 +308,7 @@
|
||||
flex-direction: row;
|
||||
padding: 0.5em 0.25em;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
border: 0;
|
||||
|
||||
&:hover {
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
display: inline-block;
|
||||
|
||||
&:hover .meta {
|
||||
opacity: 1;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
}
|
||||
@ -20,8 +20,8 @@
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
color: dark-light-choose($secondary, $primary);
|
||||
background: dark-light-choose($primary, lighten($secondary, 10%));
|
||||
color: $secondary;
|
||||
background: $primary;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
.filename {
|
||||
margin: 5px;
|
||||
margin: 6px 6px 2px 6px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
@ -45,7 +45,7 @@
|
||||
.informations {
|
||||
margin: 6px;
|
||||
padding-right: 20px;
|
||||
color: blend-primary-secondary(50%);
|
||||
color: $secondary-high;
|
||||
font-size: $font-0;
|
||||
}
|
||||
|
||||
@ -59,8 +59,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this should be removed once all the posts have been rebaked with the new lightboxes overlays
|
||||
.lightbox > span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -327,62 +327,82 @@ aside.quote {
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.btn {
|
||||
margin-right: 0.5em;
|
||||
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.participants {
|
||||
// PMs
|
||||
box-sizing: border-box;
|
||||
margin: 0 -10px;
|
||||
margin-bottom: 0.5em;
|
||||
display: flex; // IE11
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
@supports (display: grid) {
|
||||
// Overrides flex fallback above
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, auto);
|
||||
@include breakpoint(tablet, min-width) {
|
||||
grid-template-columns: repeat(5, auto);
|
||||
}
|
||||
@include breakpoint(mobile) {
|
||||
grid-template-columns: repeat(2, auto);
|
||||
font-size: $font-down-1;
|
||||
}
|
||||
@include breakpoint(mobile-small) {
|
||||
grid-template-columns: repeat(1, auto);
|
||||
align-items: center;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
&.hide-names .user {
|
||||
.username,
|
||||
.group-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
border: 1px solid $primary-low;
|
||||
border-radius: 0.25em;
|
||||
padding: 0;
|
||||
margin: 0.125em 0.25em 0.125em 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
overflow: hidden;
|
||||
flex: 0 0 auto; // IE11
|
||||
a {
|
||||
height: 26px;
|
||||
|
||||
.user-link,
|
||||
.group-link {
|
||||
color: $primary-high;
|
||||
&[href] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
span {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
&.group {
|
||||
margin: 0;
|
||||
.d-icon {
|
||||
margin-right: 4px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatar,
|
||||
.d-icon-users {
|
||||
margin-left: 0.25em;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.username,
|
||||
.group-name {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.remove-invited {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border-radius: 0 0.25em 0.25em 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.d-icon-times {
|
||||
}
|
||||
|
||||
.add-remove-participant-btn {
|
||||
.d-icon {
|
||||
margin-left: 0.25em;
|
||||
color: dark-light-choose($primary-medium, $secondary-high);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +50,11 @@
|
||||
.topic-admin-popup-menu.right-side {
|
||||
right: 50px;
|
||||
bottom: -150px; // Prevents menu from being too high when a topic is very short
|
||||
transform: translate3d(
|
||||
0,
|
||||
0,
|
||||
0
|
||||
); // iOS11 Rendering bug https://meta.discourse.org/t/wrench-menu-not-disappearing-on-ios/94297
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,7 +93,7 @@ a.badge-category {
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 4px 0;
|
||||
width: 90%;
|
||||
width: 100%;
|
||||
}
|
||||
a.badge-category {
|
||||
margin-top: 5px;
|
||||
|
||||
@ -53,9 +53,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
#second-factor {
|
||||
display: none;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -379,7 +379,7 @@ nav.post-controls {
|
||||
.avatars,
|
||||
.links,
|
||||
.information {
|
||||
padding: 7px 10px 15px 10px;
|
||||
padding: 7px 10px 7px 10px;
|
||||
color: $primary;
|
||||
}
|
||||
.buttons {
|
||||
|
||||
@ -7,7 +7,7 @@ $user_card_background: $secondary;
|
||||
#user-card,
|
||||
#group-card {
|
||||
position: absolute;
|
||||
width: 500px;
|
||||
width: 580px;
|
||||
left: -9999px;
|
||||
top: -9999px;
|
||||
z-index: z("usercard");
|
||||
@ -268,42 +268,38 @@ $user_card_background: $secondary;
|
||||
color: scale-color($user_card_background, $lightness: 70%);
|
||||
}
|
||||
|
||||
&.show-badges {
|
||||
width: 580px;
|
||||
.names {
|
||||
float: left;
|
||||
width: 45%;
|
||||
|
||||
.names {
|
||||
float: left;
|
||||
width: 45%;
|
||||
span {
|
||||
display: block;
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
width: 250px;
|
||||
}
|
||||
.badge-section {
|
||||
float: left;
|
||||
width: 500px;
|
||||
padding-bottom: 10px;
|
||||
margin-top: 5px;
|
||||
|
||||
.user-badge {
|
||||
background: $primary-very-low;
|
||||
border: 1px solid $primary-low;
|
||||
color: $user_card_primary;
|
||||
}
|
||||
|
||||
.badge-section {
|
||||
float: left;
|
||||
width: 500px;
|
||||
padding-bottom: 10px;
|
||||
margin-top: 5px;
|
||||
|
||||
.user-badge {
|
||||
background: $primary-very-low;
|
||||
border: 1px solid $primary-low;
|
||||
color: $user_card_primary;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: $user_card_background;
|
||||
font-size: $font-0;
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
h3 {
|
||||
color: $user_card_background;
|
||||
font-size: $font-0;
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
.more-user-badges {
|
||||
@extend .user-badge;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
.more-user-badges {
|
||||
@extend .user-badge;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
|
||||
.suspended {
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
@import "mobile/upload";
|
||||
@import "mobile/user";
|
||||
@import "mobile/history";
|
||||
@import "mobile/lightbox";
|
||||
@import "mobile/directory";
|
||||
@import "mobile/search";
|
||||
@import "mobile/emoji";
|
||||
|
||||
25
app/assets/stylesheets/mobile/lightbox.scss
Normal file
25
app/assets/stylesheets/mobile/lightbox.scss
Normal file
@ -0,0 +1,25 @@
|
||||
.lightbox .meta,
|
||||
.lightbox:hover .meta {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $secondary;
|
||||
color: $primary-high;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
||||
.filename,
|
||||
.informations {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.expand {
|
||||
position: initial;
|
||||
}
|
||||
}
|
||||
@ -130,9 +130,6 @@
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
#second-factor {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// styles for the
|
||||
|
||||
@ -11,7 +11,13 @@ class Admin::EmailController < Admin::AdminController
|
||||
params.require(:email_address)
|
||||
begin
|
||||
Jobs::TestEmail.new.execute(to_address: params[:email_address])
|
||||
render body: nil
|
||||
if SiteSetting.disable_emails == "yes"
|
||||
render json: { sent_test_email_message: I18n.t("admin.email.sent_test_disabled") }
|
||||
elsif SiteSetting.disable_emails == "non-staff" && !User.find_by_email(params[:email_address])&.staff?
|
||||
render json: { sent_test_email_message: I18n.t("admin.email.sent_test_disabled_for_non_staff") }
|
||||
else
|
||||
render json: { sent_test_email_message: I18n.t("admin.email.sent_test") }
|
||||
end
|
||||
rescue => e
|
||||
render json: { errors: [e.message] }, status: 422
|
||||
end
|
||||
|
||||
@ -18,44 +18,41 @@ class Admin::ReportsController < Admin::AdminController
|
||||
render_json_dump(reports: reports.sort_by { |report| report[:title] })
|
||||
end
|
||||
|
||||
def bulk
|
||||
reports = []
|
||||
|
||||
hijack do
|
||||
params[:reports].each do |report_type, report_params|
|
||||
args = parse_params(report_params)
|
||||
|
||||
report = nil
|
||||
if (report_params[:cache])
|
||||
report = Report.find_cached(report_type, args)
|
||||
end
|
||||
|
||||
if report
|
||||
reports << report
|
||||
else
|
||||
report = Report.find(report_type, args)
|
||||
|
||||
if (report_params[:cache]) && report
|
||||
Report.cache(report, 35.minutes)
|
||||
end
|
||||
|
||||
reports << report if report
|
||||
end
|
||||
end
|
||||
|
||||
render_json_dump(reports: reports)
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
report_type = params[:type]
|
||||
|
||||
raise Discourse::NotFound unless report_type =~ /^[a-z0-9\_]+$/
|
||||
|
||||
start_date = (params[:start_date].present? ? Time.parse(params[:start_date]).to_date : 1.days.ago).beginning_of_day
|
||||
end_date = (params[:end_date].present? ? Time.parse(params[:end_date]).to_date : start_date + 30.days).end_of_day
|
||||
|
||||
if params.has_key?(:category_id) && params[:category_id].to_i > 0
|
||||
category_id = params[:category_id].to_i
|
||||
else
|
||||
category_id = nil
|
||||
end
|
||||
|
||||
if params.has_key?(:group_id) && params[:group_id].to_i > 0
|
||||
group_id = params[:group_id].to_i
|
||||
else
|
||||
group_id = nil
|
||||
end
|
||||
|
||||
facets = nil
|
||||
if Array === params[:facets]
|
||||
facets = params[:facets].map { |s| s.to_s.to_sym }
|
||||
end
|
||||
|
||||
limit = nil
|
||||
if params.has_key?(:limit) && params[:limit].to_i > 0
|
||||
limit = params[:limit].to_i
|
||||
end
|
||||
|
||||
args = {
|
||||
start_date: start_date,
|
||||
end_date: end_date,
|
||||
category_id: category_id,
|
||||
group_id: group_id,
|
||||
facets: facets,
|
||||
limit: limit
|
||||
}
|
||||
args = parse_params(params)
|
||||
|
||||
report = nil
|
||||
if (params[:cache])
|
||||
@ -77,7 +74,43 @@ class Admin::ReportsController < Admin::AdminController
|
||||
|
||||
render_json_dump(report: report)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_params(report_params)
|
||||
start_date = (report_params[:start_date].present? ? Time.parse(report_params[:start_date]).to_date : 1.days.ago).beginning_of_day
|
||||
end_date = (report_params[:end_date].present? ? Time.parse(report_params[:end_date]).to_date : start_date + 30.days).end_of_day
|
||||
|
||||
if report_params.has_key?(:category_id) && report_params[:category_id].to_i > 0
|
||||
category_id = report_params[:category_id].to_i
|
||||
else
|
||||
category_id = nil
|
||||
end
|
||||
|
||||
if report_params.has_key?(:group_id) && report_params[:group_id].to_i > 0
|
||||
group_id = report_params[:group_id].to_i
|
||||
else
|
||||
group_id = nil
|
||||
end
|
||||
|
||||
facets = nil
|
||||
if Array === report_params[:facets]
|
||||
facets = report_params[:facets].map { |s| s.to_s.to_sym }
|
||||
end
|
||||
|
||||
limit = nil
|
||||
if report_params.has_key?(:limit) && report_params[:limit].to_i > 0
|
||||
limit = report_params[:limit].to_i
|
||||
end
|
||||
|
||||
{
|
||||
start_date: start_date,
|
||||
end_date: end_date,
|
||||
category_id: category_id,
|
||||
group_id: group_id,
|
||||
facets: facets,
|
||||
limit: limit
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@ -95,13 +95,13 @@ class Admin::ThemesController < Admin::AdminController
|
||||
end
|
||||
|
||||
def index
|
||||
@theme = Theme.order(:name).includes(:theme_fields, :remote_theme)
|
||||
@themes = Theme.order(:name).includes(:theme_fields, :remote_theme)
|
||||
@color_schemes = ColorScheme.all.to_a
|
||||
light = ColorScheme.new(name: I18n.t("color_schemes.light"))
|
||||
@color_schemes.unshift(light)
|
||||
|
||||
payload = {
|
||||
themes: ActiveModel::ArraySerializer.new(@theme, each_serializer: ThemeSerializer),
|
||||
themes: ActiveModel::ArraySerializer.new(@themes, each_serializer: ThemeSerializer),
|
||||
extras: {
|
||||
color_schemes: ActiveModel::ArraySerializer.new(@color_schemes, each_serializer: ColorSchemeSerializer)
|
||||
}
|
||||
@ -116,7 +116,8 @@ class Admin::ThemesController < Admin::AdminController
|
||||
@theme = Theme.new(name: theme_params[:name],
|
||||
user_id: current_user.id,
|
||||
user_selectable: theme_params[:user_selectable] || false,
|
||||
color_scheme_id: theme_params[:color_scheme_id])
|
||||
color_scheme_id: theme_params[:color_scheme_id],
|
||||
component: [true, "true"].include?(theme_params[:component]))
|
||||
set_fields
|
||||
|
||||
respond_to do |format|
|
||||
@ -155,11 +156,11 @@ class Admin::ThemesController < Admin::AdminController
|
||||
Theme.where(id: expected).each do |theme|
|
||||
@theme.add_child_theme!(theme)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
set_fields
|
||||
update_settings
|
||||
handle_switch
|
||||
|
||||
save_remote = false
|
||||
if params[:theme][:remote_check]
|
||||
@ -247,6 +248,7 @@ class Admin::ThemesController < Admin::AdminController
|
||||
:color_scheme_id,
|
||||
:default,
|
||||
:user_selectable,
|
||||
:component,
|
||||
settings: {},
|
||||
theme_fields: [:name, :target, :value, :upload_id, :type_id],
|
||||
child_theme_ids: []
|
||||
@ -280,4 +282,12 @@ class Admin::ThemesController < Admin::AdminController
|
||||
StaffActionLogger.new(current_user).log_theme_change(old_record, new_record)
|
||||
end
|
||||
|
||||
def handle_switch
|
||||
param = theme_params[:component]
|
||||
if param.to_s == "false" && @theme.component?
|
||||
@theme.switch_to_theme!
|
||||
elsif param.to_s == "true" && !@theme.component?
|
||||
@theme.switch_to_component!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -15,7 +15,11 @@ class FinishInstallationController < ApplicationController
|
||||
email = params[:email].strip
|
||||
raise Discourse::InvalidParameters.new unless @allowed_emails.include?(email)
|
||||
|
||||
return redirect_confirm(email) if UserEmail.where("lower(email) = ?", email).exists?
|
||||
if existing_user = User.find_by_email(email)
|
||||
@user = existing_user
|
||||
send_signup_email
|
||||
return redirect_confirm(email)
|
||||
end
|
||||
|
||||
@user.email = email
|
||||
@user.username = params[:username]
|
||||
@ -23,8 +27,8 @@ class FinishInstallationController < ApplicationController
|
||||
@user.password_required!
|
||||
|
||||
if @user.save
|
||||
@email_token = @user.email_tokens.unconfirmed.active.first
|
||||
Jobs.enqueue(:critical_user_email, type: :signup, user_id: @user.id, email_token: @email_token.token)
|
||||
@user.change_trust_level!(1) if @user.trust_level < 1
|
||||
send_signup_email
|
||||
return redirect_confirm(@user.email)
|
||||
end
|
||||
|
||||
@ -38,16 +42,22 @@ class FinishInstallationController < ApplicationController
|
||||
def resend_email
|
||||
@email = session[:registered_email]
|
||||
@user = User.find_by_email(@email)
|
||||
if @user.present?
|
||||
@email_token = @user.email_tokens.unconfirmed.active.first
|
||||
if @email_token.present?
|
||||
Jobs.enqueue(:critical_user_email, type: :signup, user_id: @user.id, email_token: @email_token.token)
|
||||
end
|
||||
end
|
||||
send_signup_email if @user.present?
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def send_signup_email
|
||||
email_token = @user.email_tokens.unconfirmed.active.first
|
||||
|
||||
if email_token.present?
|
||||
Jobs.enqueue(:critical_user_email,
|
||||
type: :signup,
|
||||
user_id: @user.id,
|
||||
email_token: email_token.token)
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_confirm(email)
|
||||
session[:registered_email] = email
|
||||
redirect_to(finish_installation_confirm_email_path)
|
||||
|
||||
@ -523,6 +523,12 @@ class TopicsController < ApplicationController
|
||||
|
||||
topic = Topic.find_by(id: params[:topic_id])
|
||||
|
||||
unless pm_has_slots?(topic)
|
||||
return render_json_error(I18n.t("pm_reached_recipients_limit",
|
||||
recipients_limit: SiteSetting.max_allowed_message_recipients
|
||||
))
|
||||
end
|
||||
|
||||
if topic.private_message?
|
||||
guardian.ensure_can_invite_group_to_private_message!(group, topic)
|
||||
topic.invite_group(current_user, group)
|
||||
@ -543,6 +549,12 @@ class TopicsController < ApplicationController
|
||||
group_names: params[:group_names]
|
||||
)
|
||||
|
||||
unless pm_has_slots?(topic)
|
||||
return render_json_error(I18n.t("pm_reached_recipients_limit",
|
||||
recipients_limit: SiteSetting.max_allowed_message_recipients
|
||||
))
|
||||
end
|
||||
|
||||
guardian.ensure_can_invite_to!(topic, groups)
|
||||
group_ids = groups.map(&:id)
|
||||
|
||||
@ -783,8 +795,8 @@ class TopicsController < ApplicationController
|
||||
user_id = (current_user.id if current_user)
|
||||
track_visit = should_track_visit_to_topic?
|
||||
|
||||
Scheduler::Defer.later "Track Link" do
|
||||
IncomingLink.add(
|
||||
if !request.format.json?
|
||||
hash = {
|
||||
referer: request.referer || flash[:referer],
|
||||
host: request.host,
|
||||
current_user: current_user,
|
||||
@ -792,14 +804,26 @@ class TopicsController < ApplicationController
|
||||
post_number: params[:post_number],
|
||||
username: request['u'],
|
||||
ip_address: request.remote_ip
|
||||
)
|
||||
end unless request.format.json?
|
||||
}
|
||||
# defer this way so we do not capture the whole controller
|
||||
# in the closure
|
||||
TopicsController.defer_add_incoming_link(hash)
|
||||
end
|
||||
|
||||
TopicsController.defer_track_visit(topic_id, ip, user_id, track_visit)
|
||||
end
|
||||
|
||||
def self.defer_track_visit(topic_id, ip, user_id, track_visit)
|
||||
Scheduler::Defer.later "Track Visit" do
|
||||
TopicViewItem.add(topic_id, ip, user_id)
|
||||
TopicUser.track_visit!(topic_id, user_id) if track_visit
|
||||
end
|
||||
end
|
||||
|
||||
def self.defer_add_incoming_link(hash)
|
||||
Scheduler::Defer.later "Track Link" do
|
||||
IncomingLink.add(hash)
|
||||
end
|
||||
end
|
||||
|
||||
def should_track_visit_to_topic?
|
||||
@ -868,4 +892,7 @@ class TopicsController < ApplicationController
|
||||
params[:email]
|
||||
end
|
||||
|
||||
def pm_has_slots?(pm)
|
||||
guardian.is_staff? || !pm.reached_recipients_limit?
|
||||
end
|
||||
end
|
||||
|
||||
@ -124,7 +124,7 @@ class UserAvatarsController < ApplicationController
|
||||
optimized_path = Discourse.store.path_for(optimized)
|
||||
image = optimized_path if File.exists?(optimized_path)
|
||||
else
|
||||
return proxy_avatar(Discourse.store.cdn_url(optimized.url))
|
||||
return proxy_avatar(Discourse.store.cdn_url(optimized.url), upload.created_at)
|
||||
end
|
||||
end
|
||||
|
||||
@ -141,7 +141,7 @@ class UserAvatarsController < ApplicationController
|
||||
end
|
||||
|
||||
PROXY_PATH = Rails.root + "tmp/avatar_proxy"
|
||||
def proxy_avatar(url)
|
||||
def proxy_avatar(url, last_modified)
|
||||
|
||||
if url[0..1] == "//"
|
||||
url = (SiteSetting.force_https ? "https:" : "http:") + url
|
||||
@ -163,8 +163,7 @@ class UserAvatarsController < ApplicationController
|
||||
FileUtils.mv tmp.path, path
|
||||
end
|
||||
|
||||
# putting a bogus date cause download is not retaining the data
|
||||
response.headers["Last-Modified"] = DateTime.parse("1-1-2000").httpdate
|
||||
response.headers["Last-Modified"] = last_modified.httpdate
|
||||
response.headers["Content-Length"] = File.size(path).to_s
|
||||
immutable_for(1.year)
|
||||
send_file path, disposition: nil
|
||||
@ -174,7 +173,7 @@ class UserAvatarsController < ApplicationController
|
||||
def render_blank
|
||||
path = Rails.root + "public/images/avatar.png"
|
||||
expires_in 10.minutes, public: true
|
||||
response.headers["Last-Modified"] = DateTime.parse("1-1-2000").httpdate
|
||||
response.headers["Last-Modified"] = 10.minutes.ago.httpdate
|
||||
response.headers["Content-Length"] = File.size(path).to_s
|
||||
send_file path, disposition: nil
|
||||
end
|
||||
|
||||
@ -675,7 +675,7 @@ class UsersController < ApplicationController
|
||||
if SiteSetting.enable_sso_provider && payload = cookies.delete(:sso_payload)
|
||||
return redirect_to(session_sso_provider_url + "?" + payload)
|
||||
else
|
||||
return redirect_to("/")
|
||||
return redirect_to(path('/'))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -65,6 +65,12 @@ module ApplicationHelper
|
||||
if GlobalSetting.cdn_url
|
||||
path = path.gsub(GlobalSetting.cdn_url, GlobalSetting.s3_cdn_url)
|
||||
else
|
||||
# we must remove the subfolder path here, assets are uploaded to s3
|
||||
# without it getting involved
|
||||
if ActionController::Base.config.relative_url_root
|
||||
path = path.sub(ActionController::Base.config.relative_url_root, "")
|
||||
end
|
||||
|
||||
path = "#{GlobalSetting.s3_cdn_url}#{path}"
|
||||
end
|
||||
|
||||
|
||||
@ -183,8 +183,9 @@ module Jobs
|
||||
extend MiniScheduler::Schedule
|
||||
|
||||
def perform(*args)
|
||||
return if Discourse.readonly_mode?
|
||||
super
|
||||
if (Jobs::Heartbeat === self) || !Discourse.readonly_mode?
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
15
app/jobs/onceoff/clear_width_and_height.rb
Normal file
15
app/jobs/onceoff/clear_width_and_height.rb
Normal file
@ -0,0 +1,15 @@
|
||||
module Jobs
|
||||
class ClearWidthAndHeight < Jobs::Onceoff
|
||||
def execute_onceoff(args)
|
||||
# we have to clear all old uploads cause
|
||||
# we could have old versions of height / width
|
||||
# this column used to store thumbnail size instead of
|
||||
# actual size
|
||||
DB.exec(<<~SQL)
|
||||
UPDATE uploads
|
||||
SET width = null, height = null
|
||||
WHERE width IS NOT NULL OR height IS NOT NULL
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -8,6 +8,7 @@ module Jobs
|
||||
attr_accessor :current_user
|
||||
|
||||
def initialize
|
||||
super
|
||||
@logs = []
|
||||
@sent = 0
|
||||
@failed = 0
|
||||
@ -38,13 +39,13 @@ module Jobs
|
||||
@sent += 1
|
||||
else
|
||||
# invalid email
|
||||
log "Invalid Email '#{csv_info[0]}' at line number '#{$INPUT_LINE_NUMBER}'"
|
||||
save_log "Invalid Email '#{csv_info[0]}' at line number '#{$INPUT_LINE_NUMBER}'"
|
||||
@failed += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue Exception => e
|
||||
log "Bulk Invite Process Failed -- '#{e.message}'"
|
||||
save_log "Bulk Invite Process Failed -- '#{e.message}'"
|
||||
@failed += 1
|
||||
ensure
|
||||
file.close
|
||||
@ -61,7 +62,7 @@ module Jobs
|
||||
group_ids.push(group_detail.id)
|
||||
else
|
||||
# invalid group
|
||||
log "Invalid Group '#{group_name}' at line number '#{csv_line_number}'"
|
||||
save_log "Invalid Group '#{group_name}' at line number '#{csv_line_number}'"
|
||||
@failed += 1
|
||||
end
|
||||
}
|
||||
@ -74,7 +75,7 @@ module Jobs
|
||||
if topic_id
|
||||
topic = Topic.find_by_id(topic_id)
|
||||
if topic.nil?
|
||||
log "Invalid Topic ID '#{topic_id}' at line number '#{csv_line_number}'"
|
||||
save_log "Invalid Topic ID '#{topic_id}' at line number '#{csv_line_number}'"
|
||||
@failed += 1
|
||||
end
|
||||
end
|
||||
@ -88,16 +89,12 @@ module Jobs
|
||||
begin
|
||||
Invite.invite_by_email(email, @current_user, topic, group_ids)
|
||||
rescue => e
|
||||
log "Error inviting '#{email}' -- #{Rails::Html::FullSanitizer.new.sanitize(e.message)}"
|
||||
save_log "Error inviting '#{email}' -- #{Rails::Html::FullSanitizer.new.sanitize(e.message)}"
|
||||
@sent -= 1
|
||||
@failed += 1
|
||||
end
|
||||
end
|
||||
|
||||
def log(message)
|
||||
save_log(message)
|
||||
end
|
||||
|
||||
def save_log(message)
|
||||
@logs << "[#{Time.now}] #{message}"
|
||||
end
|
||||
@ -105,9 +102,19 @@ module Jobs
|
||||
def notify_user
|
||||
if @current_user
|
||||
if (@sent > 0 && @failed == 0)
|
||||
SystemMessage.create_from_system_user(@current_user, :bulk_invite_succeeded, sent: @sent)
|
||||
SystemMessage.create_from_system_user(
|
||||
@current_user,
|
||||
:bulk_invite_succeeded,
|
||||
sent: @sent
|
||||
)
|
||||
else
|
||||
SystemMessage.create_from_system_user(@current_user, :bulk_invite_failed, sent: @sent, failed: @failed, logs: @logs.join("\n"))
|
||||
SystemMessage.create_from_system_user(
|
||||
@current_user,
|
||||
:bulk_invite_failed,
|
||||
sent: @sent,
|
||||
failed: @failed,
|
||||
logs: @logs.join("\n")
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -9,6 +9,8 @@ module Jobs
|
||||
raise Discourse::InvalidParameters.new(:invite_id) unless args[:invite_id].present?
|
||||
|
||||
invite = Invite.find_by(id: args[:invite_id])
|
||||
return unless invite.present?
|
||||
|
||||
message = InviteMailer.send_invite(invite)
|
||||
Email::Sender.new(message, :invite).send
|
||||
end
|
||||
|
||||
@ -45,6 +45,12 @@ module Jobs
|
||||
|
||||
if message
|
||||
Email::Sender.new(message, type, user).send
|
||||
if (b = user.user_stat.bounce_score) > SiteSetting.bounce_score_erode_on_send
|
||||
# erode bounce score each time we send an email
|
||||
# this means that we are punished a lot less for bounces
|
||||
# and we can recover more quickly
|
||||
user.user_stat.update(bounce_score: b - SiteSetting.bounce_score_erode_on_send)
|
||||
end
|
||||
else
|
||||
skip_reason_type
|
||||
end
|
||||
|
||||
14
app/jobs/scheduled/clean_up_post_reply_keys.rb
Normal file
14
app/jobs/scheduled/clean_up_post_reply_keys.rb
Normal file
@ -0,0 +1,14 @@
|
||||
module Jobs
|
||||
class CleanUpPostReplyKeys < Jobs::Scheduled
|
||||
every 1.day
|
||||
|
||||
def execute(_)
|
||||
return if SiteSetting.disallow_reply_by_email_after_days <= 0
|
||||
|
||||
PostReplyKey.where(
|
||||
"created_at < ?",
|
||||
SiteSetting.disallow_reply_by_email_after_days.days.ago
|
||||
).delete_all
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -624,7 +624,7 @@ end
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# name :string(50) not null
|
||||
# color :string(6) default("AB9364"), not null
|
||||
# color :string(6) default("0088CC"), not null
|
||||
# topic_id :integer
|
||||
# topic_count :integer default(0), not null
|
||||
# created_at :datetime not null
|
||||
|
||||
@ -7,17 +7,12 @@ class ChildTheme < ActiveRecord::Base
|
||||
private
|
||||
|
||||
def child_validations
|
||||
if ChildTheme.exists?(["parent_theme_id = ? OR child_theme_id = ?", child_theme_id, parent_theme_id])
|
||||
if Theme.where(
|
||||
"(component IS true AND id = :parent) OR (component IS false AND id = :child)",
|
||||
parent: parent_theme_id, child: child_theme_id
|
||||
).exists?
|
||||
errors.add(:base, I18n.t("themes.errors.no_multilevels_components"))
|
||||
end
|
||||
|
||||
if Theme.exists?(id: child_theme_id, user_selectable: true)
|
||||
errors.add(:base, I18n.t("themes.errors.component_no_user_selectable"))
|
||||
end
|
||||
|
||||
if child_theme_id == SiteSetting.default_theme_id
|
||||
errors.add(:base, I18n.t("themes.errors.component_no_default"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -60,10 +60,7 @@ class DiscourseSingleSignOn < SingleSignOn
|
||||
user.unstage
|
||||
user.save
|
||||
|
||||
# if the user isn't new or it's attached to the SSO record we might be overriding username or email
|
||||
unless user.new_record?
|
||||
change_external_attributes_and_override(sso_record, user)
|
||||
end
|
||||
change_external_attributes_and_override(sso_record, user)
|
||||
|
||||
if sso_record && (user = sso_record.user) && !user.active && !require_activation
|
||||
user.active = true
|
||||
@ -176,6 +173,10 @@ class DiscourseSingleSignOn < SingleSignOn
|
||||
ip_address: ip_address
|
||||
}
|
||||
|
||||
if SiteSetting.allow_user_locale && locale && LocaleSiteSetting.valid_value?(locale)
|
||||
user_params[:locale] = locale
|
||||
end
|
||||
|
||||
user = User.create!(user_params)
|
||||
|
||||
if SiteSetting.verbose_sso_logging
|
||||
@ -247,6 +248,10 @@ class DiscourseSingleSignOn < SingleSignOn
|
||||
user.name = name || User.suggest_name(username.blank? ? email : username)
|
||||
end
|
||||
|
||||
if locale_force_update && SiteSetting.allow_user_locale && locale && LocaleSiteSetting.valid_value?(locale)
|
||||
user.locale = locale
|
||||
end
|
||||
|
||||
avatar_missing = user.uploaded_avatar_id.nil? || !Upload.exists?(user.uploaded_avatar_id)
|
||||
|
||||
if (avatar_missing || avatar_force_update || SiteSetting.sso_overrides_avatar) && avatar_url.present?
|
||||
|
||||
@ -81,6 +81,7 @@ class OptimizedImage < ActiveRecord::Base
|
||||
end
|
||||
|
||||
if resized
|
||||
|
||||
thumbnail = OptimizedImage.create!(
|
||||
upload_id: upload.id,
|
||||
sha1: Upload.generate_digest(temp_path),
|
||||
@ -88,6 +89,7 @@ class OptimizedImage < ActiveRecord::Base
|
||||
width: width,
|
||||
height: height,
|
||||
url: "",
|
||||
filesize: File.size(temp_path)
|
||||
)
|
||||
# store the optimized image and update its url
|
||||
File.open(temp_path) do |file|
|
||||
@ -123,6 +125,32 @@ class OptimizedImage < ActiveRecord::Base
|
||||
!(url =~ /^(https?:)?\/\//)
|
||||
end
|
||||
|
||||
def calculate_filesize
|
||||
path =
|
||||
if local?
|
||||
Discourse.store.path_for(self)
|
||||
else
|
||||
Discourse.store.download(self).path
|
||||
end
|
||||
File.size(path)
|
||||
end
|
||||
|
||||
def filesize
|
||||
if size = read_attribute(:filesize)
|
||||
size
|
||||
else
|
||||
# we may have a bad optimized image so just skip for now
|
||||
# and do not break here
|
||||
size = calculate_filesize rescue nil
|
||||
|
||||
write_attribute(:filesize, size)
|
||||
if !new_record?
|
||||
update_columns(filesize: size)
|
||||
end
|
||||
size
|
||||
end
|
||||
end
|
||||
|
||||
def self.safe_path?(path)
|
||||
# this matches instructions which call #to_s
|
||||
path = path.to_s
|
||||
|
||||
@ -106,7 +106,7 @@ class Post < ActiveRecord::Base
|
||||
when 'string'
|
||||
where('raw ILIKE ?', "%#{pattern}%")
|
||||
when 'regex'
|
||||
where('raw ~ ?', "(?n)#{pattern}")
|
||||
where('raw ~* ?', "(?n)#{pattern}")
|
||||
end
|
||||
}
|
||||
|
||||
|
||||
@ -575,11 +575,11 @@ class PostAction < ActiveRecord::Base
|
||||
|
||||
def self.auto_hide_if_needed(acting_user, post, post_action_type)
|
||||
return if post.hidden?
|
||||
return if (!acting_user.staff?) && post.user.staff?
|
||||
return if (!acting_user.staff?) && post.user&.staff?
|
||||
|
||||
if post_action_type == :spam &&
|
||||
acting_user.has_trust_level?(TrustLevel[3]) &&
|
||||
post.user.trust_level == TrustLevel[0]
|
||||
post.user&.trust_level == TrustLevel[0]
|
||||
|
||||
hide_post!(post, post_action_type, Post.hidden_reasons[:flagged_by_tl3_user])
|
||||
|
||||
|
||||
@ -40,7 +40,8 @@ class RemoteTheme < ActiveRecord::Base
|
||||
importer.import!
|
||||
|
||||
theme_info = JSON.parse(importer["about.json"])
|
||||
theme = Theme.new(user_id: user&.id || -1, name: theme_info["name"])
|
||||
component = [true, "true"].include?(theme_info["component"])
|
||||
theme = Theme.new(user_id: user&.id || -1, name: theme_info["name"], component: component)
|
||||
|
||||
remote_theme = new
|
||||
theme.remote_theme = remote_theme
|
||||
@ -142,7 +143,7 @@ class RemoteTheme < ActiveRecord::Base
|
||||
self.commits_behind = 0
|
||||
end
|
||||
|
||||
update_theme_color_schemes(theme, theme_info["color_schemes"])
|
||||
update_theme_color_schemes(theme, theme_info["color_schemes"]) unless theme.component
|
||||
|
||||
self
|
||||
ensure
|
||||
|
||||
@ -3,14 +3,14 @@ require_dependency 'topic_subtype'
|
||||
class Report
|
||||
# Change this line each time report format change
|
||||
# and you want to ensure cache is reset
|
||||
SCHEMA_VERSION = 2
|
||||
SCHEMA_VERSION = 3
|
||||
|
||||
attr_accessor :type, :data, :total, :prev30Days, :start_date,
|
||||
:end_date, :category_id, :group_id, :labels, :async,
|
||||
:prev_period, :facets, :limit, :processing, :average, :percent,
|
||||
:higher_is_better, :icon, :modes, :category_filtering,
|
||||
:group_filtering, :prev_data, :prev_start_date, :prev_end_date,
|
||||
:dates_filtering, :error
|
||||
:dates_filtering, :error, :primary_color, :secondary_color
|
||||
|
||||
def self.default_days
|
||||
30
|
||||
@ -29,6 +29,8 @@ class Report
|
||||
@modes = [:table, :chart]
|
||||
@prev_data = nil
|
||||
@dates_filtering = true
|
||||
@primary_color = rgba_color(ColorScheme.hex_for_name('tertiary'))
|
||||
@secondary_color = rgba_color(ColorScheme.hex_for_name('tertiary'), 0.1)
|
||||
end
|
||||
|
||||
def self.cache_key(report)
|
||||
@ -87,6 +89,8 @@ class Report
|
||||
prev30Days: self.prev30Days,
|
||||
dates_filtering: self.dates_filtering,
|
||||
report_key: Report.cache_key(self),
|
||||
primary_color: self.primary_color,
|
||||
secondary_color: self.secondary_color,
|
||||
labels: labels || [
|
||||
{
|
||||
type: :date,
|
||||
@ -217,8 +221,9 @@ class Report
|
||||
report.icon = 'user'
|
||||
|
||||
basic_report_about report, UserVisit, :by_day, report.start_date, report.end_date, report.group_id
|
||||
|
||||
add_counts report, UserVisit, 'visited_at'
|
||||
|
||||
report.prev30Days = UserVisit.where(mobile: true).where("visited_at >= ? and visited_at < ?", report.start_date - 30.days, report.start_date).count
|
||||
end
|
||||
|
||||
def self.report_mobile_visits(report)
|
||||
@ -668,6 +673,7 @@ class Report
|
||||
report.labels = [
|
||||
{
|
||||
property: :term,
|
||||
type: :text,
|
||||
title: I18n.t("reports.trending_search.labels.term")
|
||||
},
|
||||
{
|
||||
@ -1165,4 +1171,20 @@ class Report
|
||||
report.data << revision
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def hex_to_rgbs(hex_color)
|
||||
hex_color = hex_color.gsub('#', '')
|
||||
rgbs = hex_color.scan(/../)
|
||||
rgbs
|
||||
.map! { |color| color.hex }
|
||||
.map! { |rgb| rgb.to_i }
|
||||
end
|
||||
|
||||
def rgba_color(hex, opacity = 1)
|
||||
rgbs = hex_to_rgbs(hex)
|
||||
|
||||
"rgba(#{rgbs.join(',')},#{opacity})"
|
||||
end
|
||||
end
|
||||
|
||||
@ -18,6 +18,7 @@ class S3RegionSiteSetting < EnumSiteSetting
|
||||
'ap-southeast-1',
|
||||
'ap-southeast-2',
|
||||
'cn-north-1',
|
||||
'cn-northwest-1',
|
||||
'eu-central-1',
|
||||
'eu-west-1',
|
||||
'eu-west-2',
|
||||
|
||||
@ -151,12 +151,10 @@ class SiteSetting < ActiveRecord::Base
|
||||
|
||||
# cf. http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
|
||||
if SiteSetting.s3_endpoint == "https://s3.amazonaws.com"
|
||||
if SiteSetting.Upload.s3_region == "us-east-1"
|
||||
"//#{bucket}.s3.amazonaws.com"
|
||||
elsif SiteSetting.Upload.s3_region == 'cn-north-1'
|
||||
"//#{bucket}.s3.cn-north-1.amazonaws.com.cn"
|
||||
if SiteSetting.Upload.s3_region == 'cn-north-1' || SiteSetting.Upload.s3_region == 'cn-northwest-1'
|
||||
"//#{bucket}.s3.#{SiteSetting.Upload.s3_region}.amazonaws.com.cn"
|
||||
else
|
||||
"//#{bucket}.s3-#{SiteSetting.Upload.s3_region}.amazonaws.com"
|
||||
"//#{bucket}.s3.dualstack.#{SiteSetting.Upload.s3_region}.amazonaws.com"
|
||||
end
|
||||
elsif SiteSetting.s3_force_path_style
|
||||
"//#{url_basename}/#{bucket}"
|
||||
|
||||
@ -31,7 +31,10 @@ class SkippedEmailLog < ActiveRecord::Base
|
||||
sender_message_blank: 16,
|
||||
sender_message_to_blank: 17,
|
||||
sender_text_part_body_blank: 18,
|
||||
sender_body_blank: 19
|
||||
sender_body_blank: 19,
|
||||
sender_post_deleted: 20
|
||||
# you need to add the reason in server.en.yml below the "skipped_email_log" key
|
||||
# when you add a new enum value
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ class Theme < ActiveRecord::Base
|
||||
has_many :color_schemes
|
||||
belongs_to :remote_theme
|
||||
|
||||
validate :user_selectable_validation
|
||||
validate :component_validations
|
||||
|
||||
scope :user_selectable, ->() {
|
||||
where('user_selectable OR id = ?', SiteSetting.default_theme_id)
|
||||
@ -128,7 +128,7 @@ class Theme < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def set_default!
|
||||
if component?
|
||||
if component
|
||||
raise Discourse::InvalidParameters.new(
|
||||
I18n.t("themes.errors.component_no_default")
|
||||
)
|
||||
@ -141,13 +141,36 @@ class Theme < ActiveRecord::Base
|
||||
SiteSetting.default_theme_id == id
|
||||
end
|
||||
|
||||
def component?
|
||||
ChildTheme.exists?(child_theme_id: id)
|
||||
def component_validations
|
||||
return unless component
|
||||
|
||||
errors.add(:base, I18n.t("themes.errors.component_no_color_scheme")) if color_scheme_id.present?
|
||||
errors.add(:base, I18n.t("themes.errors.component_no_user_selectable")) if user_selectable
|
||||
errors.add(:base, I18n.t("themes.errors.component_no_default")) if default?
|
||||
end
|
||||
|
||||
def user_selectable_validation
|
||||
if component? && user_selectable
|
||||
errors.add(:base, I18n.t("themes.errors.component_no_user_selectable"))
|
||||
def switch_to_component!
|
||||
return if component
|
||||
|
||||
Theme.transaction do
|
||||
self.component = true
|
||||
|
||||
self.color_scheme_id = nil
|
||||
self.user_selectable = false
|
||||
Theme.clear_default! if default?
|
||||
|
||||
ChildTheme.where("parent_theme_id = ?", id).destroy_all
|
||||
self.save!
|
||||
end
|
||||
end
|
||||
|
||||
def switch_to_theme!
|
||||
return unless component
|
||||
|
||||
Theme.transaction do
|
||||
self.component = false
|
||||
ChildTheme.where("child_theme_id = ?", id).destroy_all
|
||||
self.save!
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -801,6 +801,11 @@ class Topic < ActiveRecord::Base
|
||||
false
|
||||
end
|
||||
|
||||
def reached_recipients_limit?
|
||||
return false unless private_message?
|
||||
topic_allowed_users.count + topic_allowed_groups.count >= SiteSetting.max_allowed_message_recipients
|
||||
end
|
||||
|
||||
def invite_group(user, group)
|
||||
TopicAllowedGroup.create!(topic_id: id, group_id: group.id)
|
||||
|
||||
|
||||
@ -71,16 +71,30 @@ class TopicEmbed < ActiveRecord::Base
|
||||
else
|
||||
absolutize_urls(url, contents)
|
||||
post = embed.post
|
||||
# Update the topic if it changed
|
||||
if post && post.topic && content_sha1 != embed.content_sha1
|
||||
post_revision_args = {
|
||||
raw: absolutize_urls(url, contents),
|
||||
user_id: user.id,
|
||||
title: title,
|
||||
}
|
||||
|
||||
post.revise(user, post_revision_args, skip_validations: true, bypass_rate_limiter: true)
|
||||
embed.update_column(:content_sha1, content_sha1)
|
||||
# Update the topic if it changed
|
||||
if post&.topic
|
||||
if post.user != user
|
||||
PostOwnerChanger.new(
|
||||
post_ids: [post.id],
|
||||
topic_id: post.topic_id,
|
||||
new_owner: user,
|
||||
acting_user: Discourse.system_user
|
||||
).change_owner!
|
||||
|
||||
# make sure the post returned has the right author
|
||||
post.reload
|
||||
end
|
||||
|
||||
if content_sha1 != embed.content_sha1
|
||||
post.revise(
|
||||
user,
|
||||
{ raw: absolutize_urls(url, contents) },
|
||||
skip_validations: true,
|
||||
bypass_rate_limiter: true
|
||||
)
|
||||
embed.update!(content_sha1: content_sha1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ class TopicUser < ActiveRecord::Base
|
||||
def auto_notification(user_id, topic_id, reason, notification_level)
|
||||
should_change = TopicUser
|
||||
.where(user_id: user_id, topic_id: topic_id)
|
||||
.where("notifications_reason_id IS NULL OR (notification_level < :min AND notification_level > :max)", min: notification_level, max: notification_levels[:regular])
|
||||
.where("notifications_reason_id IS NULL OR (notification_level < :max AND notification_level > :min)", max: notification_level, min: notification_levels[:regular])
|
||||
.exists?
|
||||
|
||||
change(user_id, topic_id, notification_level: notification_level, notifications_reason_id: reason) if should_change
|
||||
@ -243,7 +243,8 @@ class TopicUser < ActiveRecord::Base
|
||||
notification_level =
|
||||
case when tu.notifications_reason_id is null and (tu.total_msecs_viewed + :msecs) >
|
||||
coalesce(uo.auto_track_topics_after_msecs,:threshold) and
|
||||
coalesce(uo.auto_track_topics_after_msecs, :threshold) >= 0 then
|
||||
coalesce(uo.auto_track_topics_after_msecs, :threshold) >= 0
|
||||
and t.archetype = 'regular' then
|
||||
:tracking
|
||||
else
|
||||
tu.notification_level
|
||||
|
||||
@ -24,7 +24,7 @@ class Upload < ActiveRecord::Base
|
||||
|
||||
validates_with ::Validators::UploadValidator
|
||||
|
||||
def thumbnail(width = self.width, height = self.height)
|
||||
def thumbnail(width = self.thumbnail_width, height = self.thumbnail_height)
|
||||
optimized_images.find_by(width: width, height: height)
|
||||
end
|
||||
|
||||
@ -41,10 +41,6 @@ class Upload < ActiveRecord::Base
|
||||
}
|
||||
|
||||
if get_optimized_image(width, height, opts)
|
||||
# TODO: this code is not right, we may have multiple
|
||||
# thumbs
|
||||
self.width = width
|
||||
self.height = height
|
||||
save(validate: false)
|
||||
end
|
||||
end
|
||||
@ -103,6 +99,51 @@ class Upload < ActiveRecord::Base
|
||||
"upload://#{Base62.encode(sha1.hex)}.#{extension}"
|
||||
end
|
||||
|
||||
def local?
|
||||
!(url =~ /^(https?:)?\/\//)
|
||||
end
|
||||
|
||||
def fix_dimensions!
|
||||
return if !FileHelper.is_image?("image.#{extension}")
|
||||
|
||||
path =
|
||||
if local?
|
||||
Discourse.store.path_for(self)
|
||||
else
|
||||
Discourse.store.download(self).path
|
||||
end
|
||||
|
||||
self.width, self.height = size = FastImage.new(path).size
|
||||
self.thumbnail_width, self.thumbnail_height = ImageSizer.resize(*size)
|
||||
nil
|
||||
end
|
||||
|
||||
# on demand image size calculation, this allows us to null out image sizes
|
||||
# and still handle as needed
|
||||
def get_dimension(key)
|
||||
if v = read_attribute(key)
|
||||
return v
|
||||
end
|
||||
fix_dimensions!
|
||||
read_attribute(key)
|
||||
end
|
||||
|
||||
def width
|
||||
get_dimension(:width)
|
||||
end
|
||||
|
||||
def height
|
||||
get_dimension(:height)
|
||||
end
|
||||
|
||||
def thumbnail_width
|
||||
get_dimension(:thumbnail_width)
|
||||
end
|
||||
|
||||
def thumbnail_height
|
||||
get_dimension(:thumbnail_height)
|
||||
end
|
||||
|
||||
def self.sha1_from_short_url(url)
|
||||
if url =~ /(upload:\/\/)?([a-zA-Z0-9]+)(\..*)?/
|
||||
sha1 = Base62.decode($2).to_s(16)
|
||||
|
||||
@ -103,17 +103,17 @@ class UserAvatar < ActiveRecord::Base
|
||||
|
||||
upload = UploadCreator.new(tempfile, "external-avatar." + ext, origin: avatar_url, type: "avatar").create_for(user.id)
|
||||
|
||||
user.create_user_avatar unless user.user_avatar
|
||||
user.create_user_avatar! unless user.user_avatar
|
||||
|
||||
if !user.user_avatar.contains_upload?(upload.id)
|
||||
user.user_avatar.update_columns(custom_upload_id: upload.id)
|
||||
|
||||
user.user_avatar.update!(custom_upload_id: upload.id)
|
||||
override_gravatar = !options || options[:override_gravatar]
|
||||
|
||||
if user.uploaded_avatar_id.nil? ||
|
||||
!user.user_avatar.contains_upload?(user.uploaded_avatar_id) ||
|
||||
override_gravatar
|
||||
user.update_columns(uploaded_avatar_id: upload.id)
|
||||
|
||||
user.update!(uploaded_avatar_id: upload.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
class BasicUserSerializer < ApplicationSerializer
|
||||
attributes :id, :username, :avatar_template
|
||||
attributes :id, :username, :name, :avatar_template
|
||||
|
||||
def name
|
||||
Hash === user ? user[:name] : user.try(:name)
|
||||
end
|
||||
|
||||
def include_name?
|
||||
SiteSetting.enable_names?
|
||||
@ -16,5 +20,4 @@ class BasicUserSerializer < ApplicationSerializer
|
||||
def user
|
||||
object[:user] || object
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user