Version bump

This commit is contained in:
Neil Lalonde 2018-08-30 10:53:41 -04:00
commit f75dc4ca65
353 changed files with 4185 additions and 1397 deletions

View File

@ -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'] && \

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
export default Ember.Component.extend({
classNames: ["themes-list"],
hasThemes: Ember.computed.gt("themes.length", 0)
});

View File

@ -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")) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -79,6 +79,7 @@
{{#each activityMetrics as |metric|}}
{{admin-report
showHeader=false
filters=activityMetricsFilters
forcedModes="counters"
dataSourceName=metric}}
{{/each}}

View File

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

View File

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

View File

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

View File

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

View File

@ -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") {

View File

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

View File

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

View File

@ -61,6 +61,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (this.get("showMore") && imageLink.length > 3) {
toolbarEvent.addText(`[![](${imageUrl})](${imageLink})`);
} else if (imageUrl.match(/\.(jpg|jpeg|png|gif)$/)) {
toolbarEvent.addText(`![](${imageUrl})`);
} else {
toolbarEvent.addText(imageUrl);
}

View File

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

View File

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

View File

@ -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(" ") +
"'>" +

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

View File

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

View File

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

View File

@ -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 `![${name}|${upload.width}x${upload.height}](${upload.short_url ||
upload.url})`;
return `![${name}|${upload.thumbnail_width}x${
upload.thumbnail_height
}](${upload.short_url || upload.url})`;
} else if (
!Discourse.SiteSettings.prevent_anons_from_downloading_files &&
/\.(mov|mp4|webm|ogv|mp3|ogg|wav|m4a)$/i.test(upload.original_filename)

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ export default RestrictedUserRoute.extend({
},
setupController: function(controller, model) {
controller.reset();
controller.setProperties({ model: model, newEmail: model.get("email") });
},

View File

@ -0,0 +1,3 @@
import staticRouteBuilder from "discourse/lib/static-route-builder";
export default staticRouteBuilder("rules");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,6 +34,11 @@
}
}
.d-icon-minus {
color: $primary-medium;
font-size: $font-down-3;
}
&.high-trending-up,
&.trending-up {
i {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -53,9 +53,10 @@
display: flex;
flex-direction: column;
}
}
#second-factor {
display: none;
&.hidden {
display: none;
}
}
}

View File

@ -379,7 +379,7 @@ nav.post-controls {
.avatars,
.links,
.information {
padding: 7px 10px 15px 10px;
padding: 7px 10px 7px 10px;
color: $primary;
}
.buttons {

View File

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

View File

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

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

View File

@ -130,9 +130,6 @@
padding: 4px 0;
}
}
#second-factor {
display: none;
}
}
// styles for the

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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