mirror of
https://github.com/discourse/discourse.git
synced 2025-06-06 03:06:53 +08:00
FEATURE: Can override any translation via an admin interface
This commit is contained in:
@ -7,6 +7,7 @@ app/assets/javascripts/vendor.js
|
|||||||
app/assets/javascripts/locales/i18n.js
|
app/assets/javascripts/locales/i18n.js
|
||||||
app/assets/javascripts/defer/html-sanitizer-bundle.js
|
app/assets/javascripts/defer/html-sanitizer-bundle.js
|
||||||
app/assets/javascripts/ember-addons/
|
app/assets/javascripts/ember-addons/
|
||||||
|
app/assets/javascripts/admin/lib/autosize.js.es6
|
||||||
lib/javascripts/locale/
|
lib/javascripts/locale/
|
||||||
lib/javascripts/messageformat.js
|
lib/javascripts/messageformat.js
|
||||||
lib/javascripts/moment.js
|
lib/javascripts/moment.js
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
import CustomizationBase from 'admin/adapters/customization-base';
|
|
||||||
export default CustomizationBase;
|
|
@ -0,0 +1,24 @@
|
|||||||
|
import { on, observes } from 'ember-addons/ember-computed-decorators';
|
||||||
|
import autosize from 'admin/lib/autosize';
|
||||||
|
|
||||||
|
export default Ember.TextArea.extend({
|
||||||
|
@on('didInsertElement')
|
||||||
|
_startWatching() {
|
||||||
|
Ember.run.scheduleOnce('afterRender', () => {
|
||||||
|
this.$().focus();
|
||||||
|
autosize(this.element);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
@observes('value')
|
||||||
|
_updateAutosize() {
|
||||||
|
const evt = document.createEvent('Event');
|
||||||
|
evt.initEvent('autosize:update', true, false);
|
||||||
|
this.element.dispatchEvent(evt);
|
||||||
|
},
|
||||||
|
|
||||||
|
@on('willDestroyElement')
|
||||||
|
_disableAutosize() {
|
||||||
|
autosize.destroy(this.$());
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,24 @@
|
|||||||
|
import { on } from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
classNames: ['site-text'],
|
||||||
|
|
||||||
|
@on('didInsertElement')
|
||||||
|
highlightTerm() {
|
||||||
|
const term = this.get('term');
|
||||||
|
if (term) {
|
||||||
|
this.$('.site-text-id, .site-text-value').highlight(term, {className: 'text-highlight'});
|
||||||
|
}
|
||||||
|
this.$('.site-text-value').ellipsis();
|
||||||
|
},
|
||||||
|
|
||||||
|
click() {
|
||||||
|
this.send('edit');
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
edit() {
|
||||||
|
this.sendAction('editAction', this.get('siteText'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -1,14 +1,29 @@
|
|||||||
export default Ember.Controller.extend({
|
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||||
saved: false,
|
import { bufferedProperty } from 'discourse/mixins/buffered-content';
|
||||||
|
|
||||||
saveDisabled: function() {
|
export default Ember.Controller.extend(bufferedProperty('siteText'), {
|
||||||
return ((!this.get('allow_blank')) && Ember.isEmpty(this.get('model.value')));
|
saved: false,
|
||||||
}.property('model.iSaving', 'model.value'),
|
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
saveChanges() {
|
saveChanges() {
|
||||||
const model = this.get('model');
|
const buffered = this.get('buffered');
|
||||||
model.save(model.getProperties('value')).then(() => this.set('saved', true));
|
this.get('siteText').save(buffered.getProperties('value')).then(() => {
|
||||||
|
this.commitBuffer();
|
||||||
|
this.set('saved', true);
|
||||||
|
}).catch(popupAjaxError);
|
||||||
|
},
|
||||||
|
|
||||||
|
revertChanges() {
|
||||||
|
this.set('saved', false);
|
||||||
|
bootbox.confirm(I18n.t('admin.site_text.revert_confirm'), result => {
|
||||||
|
if (result) {
|
||||||
|
this.get('siteText').revert().then(props => {
|
||||||
|
const buffered = this.get('buffered');
|
||||||
|
buffered.setProperties(props);
|
||||||
|
this.commitBuffer();
|
||||||
|
}).catch(popupAjaxError);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
|
export default Ember.Controller.extend({
|
||||||
|
_q: null,
|
||||||
|
searching: false,
|
||||||
|
siteTexts: null,
|
||||||
|
preferred: false,
|
||||||
|
|
||||||
|
queryParams: ['q'],
|
||||||
|
|
||||||
|
@computed
|
||||||
|
q: {
|
||||||
|
set(value) {
|
||||||
|
if (Ember.isEmpty(value)) { value = null; }
|
||||||
|
this._q = value;
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
get() {
|
||||||
|
return this._q;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_performSearch() {
|
||||||
|
const q = this.get('q');
|
||||||
|
this.store.find('site-text', { q }).then(results => {
|
||||||
|
this.set('siteTexts', results);
|
||||||
|
}).finally(() => this.set('searching', false));
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
edit(siteText) {
|
||||||
|
this.transitionToRoute('adminSiteText.edit', siteText.get('id'));
|
||||||
|
},
|
||||||
|
|
||||||
|
search() {
|
||||||
|
this.set('searching', true);
|
||||||
|
Ember.run.debounce(this, this._performSearch, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -1 +0,0 @@
|
|||||||
export default Ember.ArrayController.extend();
|
|
@ -0,0 +1,3 @@
|
|||||||
|
Em.Handlebars.helper('preserve-newlines', str => {
|
||||||
|
return new Handlebars.SafeString(Discourse.Utilities.escapeExpression(str).replace(/\n/g, "<br>"));
|
||||||
|
});
|
200
app/assets/javascripts/admin/lib/autosize.js.es6
Normal file
200
app/assets/javascripts/admin/lib/autosize.js.es6
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
const set = (typeof Set === "function") ? new Set() : (function () {
|
||||||
|
const list = [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
has(key) {
|
||||||
|
return Boolean(list.indexOf(key) > -1);
|
||||||
|
},
|
||||||
|
add(key) {
|
||||||
|
list.push(key);
|
||||||
|
},
|
||||||
|
delete(key) {
|
||||||
|
list.splice(list.indexOf(key), 1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
function assign(ta, {setOverflowX = true, setOverflowY = true} = {}) {
|
||||||
|
if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || set.has(ta)) return;
|
||||||
|
|
||||||
|
let heightOffset = null;
|
||||||
|
let overflowY = null;
|
||||||
|
let clientWidth = ta.clientWidth;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
const style = window.getComputedStyle(ta, null);
|
||||||
|
|
||||||
|
overflowY = style.overflowY;
|
||||||
|
|
||||||
|
if (style.resize === 'vertical') {
|
||||||
|
ta.style.resize = 'none';
|
||||||
|
} else if (style.resize === 'both') {
|
||||||
|
ta.style.resize = 'horizontal';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style.boxSizing === 'content-box') {
|
||||||
|
heightOffset = -(parseFloat(style.paddingTop)+parseFloat(style.paddingBottom));
|
||||||
|
} else {
|
||||||
|
heightOffset = parseFloat(style.borderTopWidth)+parseFloat(style.borderBottomWidth);
|
||||||
|
}
|
||||||
|
// Fix when a textarea is not on document body and heightOffset is Not a Number
|
||||||
|
if (isNaN(heightOffset)) {
|
||||||
|
heightOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeOverflow(value) {
|
||||||
|
{
|
||||||
|
// Chrome/Safari-specific fix:
|
||||||
|
// When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space
|
||||||
|
// made available by removing the scrollbar. The following forces the necessary text reflow.
|
||||||
|
const width = ta.style.width;
|
||||||
|
ta.style.width = '0px';
|
||||||
|
// Force reflow:
|
||||||
|
/* jshint ignore:start */
|
||||||
|
ta.offsetWidth;
|
||||||
|
/* jshint ignore:end */
|
||||||
|
ta.style.width = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
overflowY = value;
|
||||||
|
|
||||||
|
if (setOverflowY) {
|
||||||
|
ta.style.overflowY = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
const htmlTop = window.pageYOffset;
|
||||||
|
const bodyTop = document.body.scrollTop;
|
||||||
|
const originalHeight = ta.style.height;
|
||||||
|
|
||||||
|
ta.style.height = 'auto';
|
||||||
|
|
||||||
|
let endHeight = ta.scrollHeight+heightOffset;
|
||||||
|
|
||||||
|
if (ta.scrollHeight === 0) {
|
||||||
|
// If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
|
||||||
|
ta.style.height = originalHeight;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ta.style.height = endHeight+'px';
|
||||||
|
|
||||||
|
// used to check if an update is actually necessary on window.resize
|
||||||
|
clientWidth = ta.clientWidth;
|
||||||
|
|
||||||
|
// prevents scroll-position jumping
|
||||||
|
document.documentElement.scrollTop = htmlTop;
|
||||||
|
document.body.scrollTop = bodyTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
const startHeight = ta.style.height;
|
||||||
|
|
||||||
|
resize();
|
||||||
|
|
||||||
|
const style = window.getComputedStyle(ta, null);
|
||||||
|
|
||||||
|
if (style.height !== ta.style.height) {
|
||||||
|
if (overflowY !== 'visible') {
|
||||||
|
changeOverflow('visible');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (overflowY !== 'hidden') {
|
||||||
|
changeOverflow('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startHeight !== ta.style.height) {
|
||||||
|
const evt = document.createEvent('Event');
|
||||||
|
evt.initEvent('autosize:resized', true, false);
|
||||||
|
ta.dispatchEvent(evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageResize = () => {
|
||||||
|
if (ta.clientWidth !== clientWidth) {
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const destroy = style => {
|
||||||
|
window.removeEventListener('resize', pageResize, false);
|
||||||
|
ta.removeEventListener('input', update, false);
|
||||||
|
ta.removeEventListener('keyup', update, false);
|
||||||
|
ta.removeEventListener('autosize:destroy', destroy, false);
|
||||||
|
ta.removeEventListener('autosize:update', update, false);
|
||||||
|
set.delete(ta);
|
||||||
|
|
||||||
|
Object.keys(style).forEach(key => {
|
||||||
|
ta.style[key] = style[key];
|
||||||
|
});
|
||||||
|
}.bind(ta, {
|
||||||
|
height: ta.style.height,
|
||||||
|
resize: ta.style.resize,
|
||||||
|
overflowY: ta.style.overflowY,
|
||||||
|
overflowX: ta.style.overflowX,
|
||||||
|
wordWrap: ta.style.wordWrap,
|
||||||
|
});
|
||||||
|
|
||||||
|
ta.addEventListener('autosize:destroy', destroy, false);
|
||||||
|
|
||||||
|
// IE9 does not fire onpropertychange or oninput for deletions,
|
||||||
|
// so binding to onkeyup to catch most of those events.
|
||||||
|
// There is no way that I know of to detect something like 'cut' in IE9.
|
||||||
|
if ('onpropertychange' in ta && 'oninput' in ta) {
|
||||||
|
ta.addEventListener('keyup', update, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', pageResize, false);
|
||||||
|
ta.addEventListener('input', update, false);
|
||||||
|
ta.addEventListener('autosize:update', update, false);
|
||||||
|
set.add(ta);
|
||||||
|
|
||||||
|
if (setOverflowX) {
|
||||||
|
ta.style.overflowX = 'hidden';
|
||||||
|
ta.style.wordWrap = 'break-word';
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportDestroy(ta) {
|
||||||
|
if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
|
||||||
|
const evt = document.createEvent('Event');
|
||||||
|
evt.initEvent('autosize:destroy', true, false);
|
||||||
|
ta.dispatchEvent(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportUpdate(ta) {
|
||||||
|
if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
|
||||||
|
const evt = document.createEvent('Event');
|
||||||
|
evt.initEvent('autosize:update', true, false);
|
||||||
|
ta.dispatchEvent(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
let autosize = (el, options) => {
|
||||||
|
if (el) {
|
||||||
|
Array.prototype.forEach.call(el.length ? el : [el], x => assign(x, options));
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
autosize.destroy = el => {
|
||||||
|
if (el) {
|
||||||
|
Array.prototype.forEach.call(el.length ? el : [el], exportDestroy);
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
autosize.update = el => {
|
||||||
|
if (el) {
|
||||||
|
Array.prototype.forEach.call(el.length ? el : [el], exportUpdate);
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default autosize;
|
@ -1,2 +0,0 @@
|
|||||||
import RestModel from 'discourse/models/rest';
|
|
||||||
export default RestModel.extend();
|
|
@ -1,8 +1,10 @@
|
|||||||
import RestModel from 'discourse/models/rest';
|
import RestModel from 'discourse/models/rest';
|
||||||
|
const { getProperties } = Ember;
|
||||||
|
|
||||||
export default RestModel.extend({
|
export default RestModel.extend({
|
||||||
markdown: Em.computed.equal('format', 'markdown'),
|
revert() {
|
||||||
plainText: Em.computed.equal('format', 'plain'),
|
return Discourse.ajax(`/admin/customize/site_texts/${this.get('id')}`, {
|
||||||
html: Em.computed.equal('format', 'html'),
|
method: 'DELETE'
|
||||||
css: Em.computed.equal('format', 'css'),
|
}).then(result => getProperties(result.site_text, 'value', 'can_revert'));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -22,8 +22,9 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.resource('adminSiteText', { path: '/site_texts' }, function() {
|
this.resource('adminSiteText', { path: '/site_texts' }, function() {
|
||||||
this.route('edit', {path: '/:text_type'});
|
this.route('edit', { path: '/:id' });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.resource('adminUserFields', { path: '/user_fields' });
|
this.resource('adminUserFields', { path: '/user_fields' });
|
||||||
this.resource('adminEmojis', { path: '/emojis' });
|
this.resource('adminEmojis', { path: '/emojis' });
|
||||||
this.resource('adminPermalinks', { path: '/permalinks' });
|
this.resource('adminPermalinks', { path: '/permalinks' });
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
export default Discourse.Route.extend({
|
export default Ember.Route.extend({
|
||||||
model(params) {
|
model(params) {
|
||||||
return this.store.find('site-text', params.text_type);
|
return this.store.find('site-text', params.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController(controller, siteText) {
|
||||||
|
controller.setProperties({ siteText, saved: false });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
export default Ember.Route.extend({
|
||||||
|
queryParams: {
|
||||||
|
q: { replace: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
model(params) {
|
||||||
|
return this.store.find('site-text', { q: params.q });
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController(controller, model) {
|
||||||
|
controller.set('siteTexts', model);
|
||||||
|
}
|
||||||
|
});
|
@ -1,5 +0,0 @@
|
|||||||
export default Discourse.Route.extend({
|
|
||||||
model() {
|
|
||||||
return this.store.findAll('site-text-type');
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,5 +1,7 @@
|
|||||||
{{d-button action="saveChanges" disabled=buttonDisabled label=savingText class="btn-primary"}}
|
{{d-button action="saveChanges" disabled=buttonDisabled label=savingText class="btn-primary save-changes"}}
|
||||||
{{yield}}
|
{{yield}}
|
||||||
<div class='save-messages'>
|
<div class='save-messages'>
|
||||||
{{#if saved}}{{i18n 'saved'}}{{/if}}
|
{{#if saved}}
|
||||||
|
<div class='saved'>{{i18n 'saved'}}</div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
{{d-button label="admin.site_text.edit" class='edit' action="edit"}}
|
||||||
|
<h3 class='site-text-id'>{{siteText.id}}</h3>
|
||||||
|
<div class='site-text-value'>{{siteText.value}}</div>
|
||||||
|
|
||||||
|
<div class='clearfix'></div>
|
@ -1,17 +1,20 @@
|
|||||||
<h3>{{model.title}}</h3>
|
<div class='edit-site-text'>
|
||||||
<p class='description'>{{model.description}}</p>
|
|
||||||
|
|
||||||
{{#if model.markdown}}
|
<div class='title'>
|
||||||
{{d-editor value=model.value}}
|
<h3>{{siteText.id}}</h3>
|
||||||
{{/if}}
|
</div>
|
||||||
{{#if model.plainText}}
|
|
||||||
{{textarea value=model.value class="plain"}}
|
|
||||||
{{/if}}
|
|
||||||
{{#if model.html}}
|
|
||||||
{{ace-editor content=model.value mode="html"}}
|
|
||||||
{{/if}}
|
|
||||||
{{#if model.css}}
|
|
||||||
{{ace-editor content=model.value mode="css"}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{save-controls model=model action="saveChanges" saveDisabled=saveDisabled saved=saved}}
|
{{expanding-text-area value=buffered.value rows="1" class="site-text-value"}}
|
||||||
|
|
||||||
|
{{#save-controls model=siteText action="saveChanges" saved=saved}}
|
||||||
|
{{#if siteText.can_revert}}
|
||||||
|
{{d-button action="revertChanges" label="admin.site_text.revert" class="revert-site-text"}}
|
||||||
|
{{/if}}
|
||||||
|
{{/save-controls}}
|
||||||
|
|
||||||
|
{{#link-to 'adminSiteText.index' class="go-back"}}
|
||||||
|
{{fa-icon 'arrow-left'}}
|
||||||
|
{{i18n 'admin.site_text.go_back'}}
|
||||||
|
{{/link-to}}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@ -1 +1,19 @@
|
|||||||
<p>{{i18n 'admin.site_text.none'}}</p>
|
<div class='search-area'>
|
||||||
|
<p>{{i18n "admin.site_text.description"}}</p>
|
||||||
|
|
||||||
|
{{text-field value=q
|
||||||
|
placeholderKey="admin.site_text.search"
|
||||||
|
class="no-blur site-text-search"
|
||||||
|
autofocus="true"
|
||||||
|
keyUpAction="search"}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#conditional-loading-spinner condition=searching}}
|
||||||
|
{{#unless siteTexts.findArgs.q}}
|
||||||
|
<p><b>{{i18n "admin.site_text.recommended"}}</b></p>
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
{{#each siteTexts as |siteText|}}
|
||||||
|
{{site-text-summary siteText=siteText editAction="edit" term=q}}
|
||||||
|
{{/each}}
|
||||||
|
{{/conditional-loading-spinner}}
|
||||||
|
@ -1,15 +1,3 @@
|
|||||||
<div class='row'>
|
<div class='row site-texts'>
|
||||||
<div class='content-list span6'>
|
{{outlet}}
|
||||||
<ul>
|
|
||||||
{{#each c in model}}
|
|
||||||
<li>
|
|
||||||
{{#link-to 'adminSiteText.edit' c.text_type}}{{c.title}}{{/link-to}}
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='content-editor'>
|
|
||||||
{{outlet}}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import { on } from "ember-addons/ember-computed-decorators";
|
|
||||||
|
|
||||||
export default Ember.TextField.extend({
|
|
||||||
|
|
||||||
@on("didInsertElement")
|
|
||||||
becomeFocused() {
|
|
||||||
const input = this.get("element");
|
|
||||||
input.focus();
|
|
||||||
input.selectionStart = input.selectionEnd = input.value.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
@ -6,5 +6,12 @@ export default Ember.TextField.extend({
|
|||||||
@computed("placeholderKey")
|
@computed("placeholderKey")
|
||||||
placeholder(placeholderKey) {
|
placeholder(placeholderKey) {
|
||||||
return placeholderKey ? I18n.t(placeholderKey) : "";
|
return placeholderKey ? I18n.t(placeholderKey) : "";
|
||||||
|
},
|
||||||
|
|
||||||
|
keyUp() {
|
||||||
|
const act = this.get('keyUpAction');
|
||||||
|
if (act) {
|
||||||
|
this.sendAction('keyUpAction');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -12,6 +12,7 @@ export default {
|
|||||||
const overrides = PreloadStore.get('translationOverrides') || {};
|
const overrides = PreloadStore.get('translationOverrides') || {};
|
||||||
Object.keys(overrides).forEach(k => {
|
Object.keys(overrides).forEach(k => {
|
||||||
const v = overrides[k];
|
const v = overrides[k];
|
||||||
|
k = k.replace('admin_js', 'js');
|
||||||
|
|
||||||
const segs = k.split('.');
|
const segs = k.split('.');
|
||||||
let node = I18n.translations[I18n.locale];
|
let node = I18n.translations[I18n.locale];
|
||||||
|
@ -4,6 +4,13 @@ export default Ember.ArrayProxy.extend({
|
|||||||
totalRows: 0,
|
totalRows: 0,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
|
|
||||||
|
content: null,
|
||||||
|
loadMoreUrl: null,
|
||||||
|
refreshUrl: null,
|
||||||
|
findArgs: null,
|
||||||
|
store: null,
|
||||||
|
__type: null,
|
||||||
|
|
||||||
canLoadMore: function() {
|
canLoadMore: function() {
|
||||||
return this.get('length') < this.get('totalRows');
|
return this.get('length') < this.get('totalRows');
|
||||||
}.property('totalRows', 'length'),
|
}.property('totalRows', 'length'),
|
||||||
|
@ -63,7 +63,7 @@ export default Ember.Object.extend({
|
|||||||
|
|
||||||
_hydrateFindResults(result, type, findArgs) {
|
_hydrateFindResults(result, type, findArgs) {
|
||||||
if (typeof findArgs === "object") {
|
if (typeof findArgs === "object") {
|
||||||
return this._resultSet(type, result);
|
return this._resultSet(type, result, findArgs);
|
||||||
} else {
|
} else {
|
||||||
return this._hydrate(type, result[Ember.String.underscore(type)], result);
|
return this._hydrate(type, result[Ember.String.underscore(type)], result);
|
||||||
}
|
}
|
||||||
@ -81,7 +81,7 @@ export default Ember.Object.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
find(type, findArgs, opts) {
|
find(type, findArgs, opts) {
|
||||||
return this.adapterFor(type).find(this, type, findArgs, opts).then((result) => {
|
return this.adapterFor(type).find(this, type, findArgs, opts).then(result => {
|
||||||
return this._hydrateFindResults(result, type, findArgs, opts);
|
return this._hydrateFindResults(result, type, findArgs, opts);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -142,14 +142,14 @@ export default Ember.Object.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_resultSet(type, result) {
|
_resultSet(type, result, findArgs) {
|
||||||
const typeName = Ember.String.underscore(this.pluralize(type)),
|
const typeName = Ember.String.underscore(this.pluralize(type)),
|
||||||
content = result[typeName].map(obj => this._hydrate(type, obj, result)),
|
content = result[typeName].map(obj => this._hydrate(type, obj, result)),
|
||||||
totalRows = result["total_rows_" + typeName] || content.length,
|
totalRows = result["total_rows_" + typeName] || content.length,
|
||||||
loadMoreUrl = result["load_more_" + typeName],
|
loadMoreUrl = result["load_more_" + typeName],
|
||||||
refreshUrl = result['refresh_' + typeName];
|
refreshUrl = result['refresh_' + typeName];
|
||||||
|
|
||||||
return ResultSet.create({ content, totalRows, loadMoreUrl, refreshUrl, store: this, __type: type });
|
return ResultSet.create({ content, totalRows, loadMoreUrl, refreshUrl, findArgs, store: this, __type: type });
|
||||||
},
|
},
|
||||||
|
|
||||||
_build(type, obj) {
|
_build(type, obj) {
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
{{#if model.isPrivateMessage}}
|
{{#if model.isPrivateMessage}}
|
||||||
<span class="private-message-glyph">{{fa-icon "envelope"}}</span>
|
<span class="private-message-glyph">{{fa-icon "envelope"}}</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{autofocus-text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length}}
|
|
||||||
|
{{text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length autofocus="true"}}
|
||||||
{{#if showCategoryChooser}}
|
{{#if showCategoryChooser}}
|
||||||
<br>
|
<br>
|
||||||
{{category-chooser valueAttribute="id" value=buffered.category_id source=buffered.category_id}}
|
{{category-chooser valueAttribute="id" value=buffered.category_id source=buffered.category_id}}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
//= require admin/routes/admin-email-logs
|
//= require admin/routes/admin-email-logs
|
||||||
//= require admin/controllers/admin-email-skipped
|
//= require admin/controllers/admin-email-skipped
|
||||||
//= require discourse/lib/export-result
|
//= require discourse/lib/export-result
|
||||||
|
//= require_tree ./admin/lib
|
||||||
//= require_tree ./admin
|
//= require_tree ./admin
|
||||||
|
|
||||||
//= require resumable.js
|
//= require resumable.js
|
||||||
|
@ -43,6 +43,59 @@ td.flaggers td {
|
|||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.site-texts {
|
||||||
|
.search-area {
|
||||||
|
margin-bottom: 2em;
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: 0.5em;
|
||||||
|
font-size: 1em;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.text-highlight {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-text {
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.edit {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.site-text-value {
|
||||||
|
margin: 0.5em 5em 0.5em 0;
|
||||||
|
max-height: 100px;
|
||||||
|
color: dark-light-diff($primary, $secondary, 40%, -10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-site-text {
|
||||||
|
textarea {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-messages, .title {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.go-back {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.content-list li a span.count {
|
.content-list li a span.count {
|
||||||
font-size: 0.857em;
|
font-size: 0.857em;
|
||||||
float: right;
|
float: right;
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
class Admin::SiteTextTypesController < Admin::AdminController
|
|
||||||
|
|
||||||
def index
|
|
||||||
render_serialized(SiteText.text_types, SiteTextTypeSerializer, root: 'site_text_types')
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
@ -1,21 +1,54 @@
|
|||||||
class Admin::SiteTextsController < Admin::AdminController
|
class Admin::SiteTextsController < Admin::AdminController
|
||||||
|
|
||||||
|
def self.preferred_keys
|
||||||
|
['system_messages.usage_tips.text_body_template',
|
||||||
|
'education.new-topic',
|
||||||
|
'education.new-reply',
|
||||||
|
'login_required.welcome_message']
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
if params[:q].blank?
|
||||||
|
results = self.class.preferred_keys.map {|k| {id: k, value: I18n.t(k) }}
|
||||||
|
else
|
||||||
|
results = []
|
||||||
|
translations = I18n.search(params[:q])
|
||||||
|
translations.each do |k, v|
|
||||||
|
results << {id: k, value: v}
|
||||||
|
end
|
||||||
|
results.sort! do |x, y|
|
||||||
|
(x[:id].size + x[:value].size) <=> (y[:id].size + y[:value].size)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render_serialized(results[0..50], SiteTextSerializer, root: 'site_texts', rest_serializer: true)
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
site_text = SiteText.find_or_new(params[:id].to_s)
|
site_text = find_site_text
|
||||||
render_serialized(site_text, SiteTextSerializer, root: 'site_text')
|
render_serialized(site_text, SiteTextSerializer, root: 'site_text', rest_serializer: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
site_text = SiteText.find_or_new(params[:id].to_s)
|
site_text = find_site_text
|
||||||
|
site_text[:value] = params[:site_text][:value]
|
||||||
|
|
||||||
# Updating to nothing is the same as removing it
|
TranslationOverride.upsert!(I18n.locale, site_text[:id], site_text[:value])
|
||||||
if params[:site_text][:value].present?
|
render_serialized(site_text, SiteTextSerializer, root: 'site_text', rest_serializer: true)
|
||||||
site_text.value = params[:site_text][:value]
|
end
|
||||||
site_text.save!
|
|
||||||
else
|
def revert
|
||||||
site_text.destroy
|
site_text = find_site_text
|
||||||
|
TranslationOverride.revert!(I18n.locale, site_text[:id])
|
||||||
|
site_text = find_site_text
|
||||||
|
render_serialized(site_text, SiteTextSerializer, root: 'site_text', rest_serializer: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def find_site_text
|
||||||
|
raise Discourse::NotFound unless I18n.exists?(params[:id])
|
||||||
|
{id: params[:id], value: I18n.t(params[:id]) }
|
||||||
end
|
end
|
||||||
|
|
||||||
render_serialized(site_text, SiteTextSerializer, root: 'site_text')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -149,18 +149,6 @@ module ApplicationHelper
|
|||||||
result.join("\n")
|
result.join("\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Look up site content for a key. If the key is blank, you can supply a block and that
|
|
||||||
# will be rendered instead.
|
|
||||||
def markdown_content(key, replacements=nil)
|
|
||||||
result = PrettyText.cook(SiteText.text_for(key, replacements || {})).html_safe
|
|
||||||
if result.blank? && block_given?
|
|
||||||
yield
|
|
||||||
nil
|
|
||||||
else
|
|
||||||
result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def application_logo_url
|
def application_logo_url
|
||||||
@application_logo_url ||= (mobile_view? && SiteSetting.mobile_logo_url) || SiteSetting.logo_url
|
@application_logo_url ||= (mobile_view? && SiteSetting.mobile_logo_url) || SiteSetting.logo_url
|
||||||
end
|
end
|
||||||
|
@ -18,7 +18,7 @@ class UserNotifications < ActionMailer::Base
|
|||||||
build_email(user.email,
|
build_email(user.email,
|
||||||
template: 'user_notifications.signup_after_approval',
|
template: 'user_notifications.signup_after_approval',
|
||||||
email_token: opts[:email_token],
|
email_token: opts[:email_token],
|
||||||
new_user_tips: SiteText.text_for(:usage_tips, base_url: Discourse.base_url))
|
new_user_tips: I18n.t('system_messages.usage_tips.text_body_template', base_url: Discourse.base_url))
|
||||||
end
|
end
|
||||||
|
|
||||||
def authorize_email(user, opts={})
|
def authorize_email(user, opts={})
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
require_dependency 'site_text_type'
|
|
||||||
require_dependency 'site_text_class_methods'
|
|
||||||
require_dependency 'distributed_cache'
|
|
||||||
|
|
||||||
class SiteText < ActiveRecord::Base
|
|
||||||
extend SiteTextClassMethods
|
|
||||||
|
|
||||||
self.primary_key = 'text_type'
|
|
||||||
|
|
||||||
validates_presence_of :value
|
|
||||||
|
|
||||||
after_save do
|
|
||||||
SiteText.text_for_cache.clear
|
|
||||||
end
|
|
||||||
|
|
||||||
after_destroy do
|
|
||||||
SiteText.text_for_cache.clear
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.formats
|
|
||||||
@formats ||= Enum.new(:plain, :markdown, :html, :css)
|
|
||||||
end
|
|
||||||
|
|
||||||
add_text_type :usage_tips, default_18n_key: 'system_messages.usage_tips.text_body_template'
|
|
||||||
add_text_type :education_new_topic, default_18n_key: 'education.new-topic'
|
|
||||||
add_text_type :education_new_reply, default_18n_key: 'education.new-reply'
|
|
||||||
add_text_type :login_required_welcome_message, default_18n_key: 'login_required.welcome_message'
|
|
||||||
|
|
||||||
def site_text_type
|
|
||||||
@site_text_type ||= SiteText.find_text_type(text_type)
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
# == Schema Information
|
|
||||||
#
|
|
||||||
# Table name: site_texts
|
|
||||||
#
|
|
||||||
# text_type :string(255) not null, primary key
|
|
||||||
# value :text not null
|
|
||||||
# created_at :datetime not null
|
|
||||||
# updated_at :datetime not null
|
|
||||||
#
|
|
||||||
# Indexes
|
|
||||||
#
|
|
||||||
# index_site_texts_on_text_type (text_type) UNIQUE
|
|
||||||
#
|
|
@ -1,27 +0,0 @@
|
|||||||
class SiteTextType
|
|
||||||
|
|
||||||
attr_accessor :text_type, :format
|
|
||||||
|
|
||||||
def initialize(text_type, format, opts=nil)
|
|
||||||
@opts = opts || {}
|
|
||||||
@text_type = text_type
|
|
||||||
@format = format
|
|
||||||
end
|
|
||||||
|
|
||||||
def title
|
|
||||||
I18n.t("content_types.#{text_type}.title")
|
|
||||||
end
|
|
||||||
|
|
||||||
def description
|
|
||||||
I18n.t("content_types.#{text_type}.description")
|
|
||||||
end
|
|
||||||
|
|
||||||
def allow_blank?
|
|
||||||
!!@opts[:allow_blank]
|
|
||||||
end
|
|
||||||
|
|
||||||
def default_text
|
|
||||||
@opts[:default_18n_key].present? ? I18n.t(@opts[:default_18n_key]) : ""
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
@ -1,39 +1,20 @@
|
|||||||
class SiteTextSerializer < ApplicationSerializer
|
class SiteTextSerializer < ApplicationSerializer
|
||||||
|
attributes :id, :value, :can_revert?
|
||||||
attributes :id,
|
|
||||||
:text_type,
|
|
||||||
:title,
|
|
||||||
:description,
|
|
||||||
:value,
|
|
||||||
:format,
|
|
||||||
:allow_blank?
|
|
||||||
|
|
||||||
def id
|
def id
|
||||||
text_type
|
object[:id]
|
||||||
end
|
|
||||||
|
|
||||||
def title
|
|
||||||
object.site_text_type.title
|
|
||||||
end
|
|
||||||
|
|
||||||
def text_type
|
|
||||||
object.text_type
|
|
||||||
end
|
|
||||||
|
|
||||||
def description
|
|
||||||
object.site_text_type.description
|
|
||||||
end
|
|
||||||
|
|
||||||
def format
|
|
||||||
object.site_text_type.format
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def value
|
def value
|
||||||
return object.value if object.value.present?
|
object[:value]
|
||||||
object.site_text_type.default_text
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def allow_blank?
|
def can_revert?
|
||||||
object.site_text_type.allow_blank?
|
current_val = value
|
||||||
|
|
||||||
|
I18n.overrides_disabled do
|
||||||
|
return I18n.t(object[:id]) != current_val
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
class SiteTextTypeSerializer < ApplicationSerializer
|
|
||||||
|
|
||||||
attributes :id, :text_type, :title
|
|
||||||
|
|
||||||
def id
|
|
||||||
text_type
|
|
||||||
end
|
|
||||||
|
|
||||||
def text_type
|
|
||||||
object.text_type
|
|
||||||
end
|
|
||||||
|
|
||||||
def title
|
|
||||||
object.title
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
@ -1,3 +1,3 @@
|
|||||||
<% if SiteSetting.login_required %>
|
<% if SiteSetting.login_required %>
|
||||||
<%= markdown_content(:login_required_welcome_message) %>
|
<%= PrettyText.cook(I18n.t('login_required.welcome_message')).html_safe %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
require 'i18n/backend/discourse_i18n'
|
require 'i18n/backend/discourse_i18n'
|
||||||
I18n.backend = I18n::Backend::DiscourseI18n.new
|
I18n.backend = I18n::Backend::DiscourseI18n.new
|
||||||
I18n.config.missing_interpolation_argument_handler = proc { throw(:exception) }
|
I18n.config.missing_interpolation_argument_handler = proc { throw(:exception) }
|
||||||
I18n.reload!
|
I18n.init_accelerator!
|
||||||
|
|
||||||
MessageBus.subscribe("/i18n-flush") { I18n.reload! }
|
unless Rails.env.test?
|
||||||
|
MessageBus.subscribe("/i18n-flush") { I18n.reload! }
|
||||||
|
end
|
||||||
|
@ -2482,8 +2482,14 @@ en:
|
|||||||
dropdown: "Dropdown"
|
dropdown: "Dropdown"
|
||||||
|
|
||||||
site_text:
|
site_text:
|
||||||
none: "Choose a type of content to begin editing."
|
description: "You can customize any of the text on your forum. Please start by searching below:"
|
||||||
|
search: "Search for the text you'd like to edit"
|
||||||
title: 'Text Content'
|
title: 'Text Content'
|
||||||
|
edit: 'edit'
|
||||||
|
revert: "Revert Changes"
|
||||||
|
revert_confirm: "Are you sure you want to revert your changes?"
|
||||||
|
go_back: "Back to Search"
|
||||||
|
recommended: "We recommend customizing the following text to suit your needs:"
|
||||||
|
|
||||||
site_settings:
|
site_settings:
|
||||||
show_overriden: 'Only show overridden'
|
show_overriden: 'Only show overridden'
|
||||||
|
@ -748,38 +748,6 @@ en:
|
|||||||
notification_email_warning: "Notification emails are not being sent from a valid email address on your domain; email delivery will be erratic and unreliable. Please set notification_email to a valid local email address in <a href='/admin/site_settings'>Site Settings</a>."
|
notification_email_warning: "Notification emails are not being sent from a valid email address on your domain; email delivery will be erratic and unreliable. Please set notification_email to a valid local email address in <a href='/admin/site_settings'>Site Settings</a>."
|
||||||
subfolder_ends_in_slash: "Your subfolder setup is incorrect; the DISCOURSE_RELATIVE_URL_ROOT ends in a slash."
|
subfolder_ends_in_slash: "Your subfolder setup is incorrect; the DISCOURSE_RELATIVE_URL_ROOT ends in a slash."
|
||||||
|
|
||||||
content_types:
|
|
||||||
education_new_reply:
|
|
||||||
title: "New User Education: First Replies"
|
|
||||||
description: "Pop up just-in-time guidance automatically displayed above the composer when new users begin typing their first two new replies."
|
|
||||||
education_new_topic:
|
|
||||||
title: "New User Education: First Topics"
|
|
||||||
description: "Pop up just-in-time guidance automatically displayed above the composer when new users begin typing their first two new topics."
|
|
||||||
usage_tips:
|
|
||||||
title: "New User Guidance"
|
|
||||||
description: "Guidance and essential information for new users."
|
|
||||||
welcome_user:
|
|
||||||
title: "Welcome: New User"
|
|
||||||
description: "A message automatically sent to all new users when they sign up."
|
|
||||||
welcome_invite:
|
|
||||||
title: "Welcome: Invited User"
|
|
||||||
description: "A message automatically sent to all new invited users when they accept the invitation from another user to participate."
|
|
||||||
login_required_welcome_message:
|
|
||||||
title: "Login Required: Welcome Message"
|
|
||||||
description: "Welcome message that is displayed to logged out users when the 'login required' setting is enabled."
|
|
||||||
login_required:
|
|
||||||
title: "Login Required: Homepage"
|
|
||||||
description: "The text displayed for unauthorized users when login is required on the site."
|
|
||||||
head:
|
|
||||||
title: "HTML head"
|
|
||||||
description: "HTML that will be inserted inside the <head></head> tags."
|
|
||||||
top:
|
|
||||||
title: "Top of the pages"
|
|
||||||
description: "HTML that will be added at the top of every page (after the header, before the navigation or the topic title)."
|
|
||||||
bottom:
|
|
||||||
title: "Bottom of the pages"
|
|
||||||
description: "HTML that will be added before the </body> tag."
|
|
||||||
|
|
||||||
site_settings:
|
site_settings:
|
||||||
censored_words: "Words that will be automatically replaced with ■■■■"
|
censored_words: "Words that will be automatically replaced with ■■■■"
|
||||||
delete_old_hidden_posts: "Auto-delete any hidden posts that stay hidden for more than 30 days."
|
delete_old_hidden_posts: "Auto-delete any hidden posts that stay hidden for more than 30 days."
|
||||||
|
@ -154,12 +154,15 @@ Discourse::Application.routes.draw do
|
|||||||
post "flags/defer/:id" => "flags#defer"
|
post "flags/defer/:id" => "flags#defer"
|
||||||
resources :site_customizations, constraints: AdminConstraint.new
|
resources :site_customizations, constraints: AdminConstraint.new
|
||||||
scope "/customize" do
|
scope "/customize" do
|
||||||
resources :site_texts, constraints: AdminConstraint.new
|
|
||||||
resources :site_text_types, constraints: AdminConstraint.new
|
|
||||||
resources :user_fields, constraints: AdminConstraint.new
|
resources :user_fields, constraints: AdminConstraint.new
|
||||||
resources :emojis, constraints: AdminConstraint.new
|
resources :emojis, constraints: AdminConstraint.new
|
||||||
|
|
||||||
# They have periods in their URLs often:
|
# They have periods in their URLs often:
|
||||||
|
get 'site_texts' => 'site_texts#index'
|
||||||
|
match 'site_texts/(:id)' => 'site_texts#show', :constraints => { :id => /[0-9a-z\_\.\-]+/ }, via: :get
|
||||||
|
match 'site_texts/(:id)' => 'site_texts#update', :constraints => { :id => /[0-9a-z\_\.\-]+/ }, via: :put
|
||||||
|
match 'site_texts/(:id)' => 'site_texts#revert', :constraints => { :id => /[0-9a-z\_\.\-]+/ }, via: :delete
|
||||||
|
|
||||||
get 'email_templates' => 'email_templates#index'
|
get 'email_templates' => 'email_templates#index'
|
||||||
match 'email_templates/(:id)' => 'email_templates#show', :constraints => { :id => /[0-9a-z\_\.]+/ }, via: :get
|
match 'email_templates/(:id)' => 'email_templates#show', :constraints => { :id => /[0-9a-z\_\.]+/ }, via: :get
|
||||||
match 'email_templates/(:id)' => 'email_templates#update', :constraints => { :id => /[0-9a-z\_\.]+/ }, via: :put
|
match 'email_templates/(:id)' => 'email_templates#update', :constraints => { :id => /[0-9a-z\_\.]+/ }, via: :put
|
||||||
|
@ -32,11 +32,9 @@ unless Rails.env.test?
|
|||||||
company_name: "company_short_name"
|
company_name: "company_short_name"
|
||||||
})
|
})
|
||||||
|
|
||||||
create_static_page_topic('guidelines_topic_id', 'guidelines_topic.title', "guidelines_topic.body",
|
create_static_page_topic('guidelines_topic_id', 'guidelines_topic.title', "guidelines_topic.body", nil, staff, "guidelines")
|
||||||
(SiteText.text_for(:faq) rescue nil), staff, "guidelines")
|
|
||||||
|
|
||||||
create_static_page_topic('privacy_topic_id', 'privacy_topic.title', "privacy_topic.body",
|
create_static_page_topic('privacy_topic_id', 'privacy_topic.title', "privacy_topic.body", nil, staff, "privacy policy")
|
||||||
(SiteText.text_for(:privacy_policy) rescue nil), staff, "privacy policy")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if seed_welcome_topics
|
if seed_welcome_topics
|
||||||
|
21
db/migrate/20151125194322_remove_site_text.rb
Normal file
21
db/migrate/20151125194322_remove_site_text.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
class RemoveSiteText < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
execute "INSERT INTO translation_overrides (locale, translation_key, value, created_at, updated_at)
|
||||||
|
SELECT '#{I18n.locale}',
|
||||||
|
CASE
|
||||||
|
WHEN text_type = 'usage_tips' THEN 'system_messages.usage_tips.text_body_template'
|
||||||
|
WHEN text_type = 'education_new_topic' THEN 'education.new-topic'
|
||||||
|
WHEN text_type = 'education_new_reply' THEN 'education.new-reply'
|
||||||
|
WHEN text_type = 'login_required_welcome_message' THEN 'login_required.welcome_message'
|
||||||
|
END,
|
||||||
|
value,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM site_texts
|
||||||
|
WHERE text_type in ('usage_tips',
|
||||||
|
'education_new_topic',
|
||||||
|
'education_new_reply',
|
||||||
|
'login_required_welcome_message')"
|
||||||
|
drop_table :site_texts
|
||||||
|
end
|
||||||
|
end
|
@ -18,10 +18,10 @@ class ComposerMessagesFinder
|
|||||||
def check_education_message
|
def check_education_message
|
||||||
if creating_topic?
|
if creating_topic?
|
||||||
count = @user.created_topic_count
|
count = @user.created_topic_count
|
||||||
education_key = :education_new_topic
|
education_key = 'education.new_topic'
|
||||||
else
|
else
|
||||||
count = @user.post_count
|
count = @user.post_count
|
||||||
education_key = :education_new_reply
|
education_key = 'education.new_reply'
|
||||||
end
|
end
|
||||||
|
|
||||||
if count < SiteSetting.educate_until_posts
|
if count < SiteSetting.educate_until_posts
|
||||||
@ -29,7 +29,7 @@ class ComposerMessagesFinder
|
|||||||
return {
|
return {
|
||||||
templateName: 'composer/education',
|
templateName: 'composer/education',
|
||||||
wait_for_typing: true,
|
wait_for_typing: true,
|
||||||
body: PrettyText.cook(SiteText.text_for(education_key, education_posts_text: education_posts_text))
|
body: PrettyText.cook(I18n.t(education_key, education_posts_text: education_posts_text))
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -13,14 +13,18 @@ module I18n
|
|||||||
# this accelerates translation a tiny bit (halves the time it takes)
|
# this accelerates translation a tiny bit (halves the time it takes)
|
||||||
class << self
|
class << self
|
||||||
alias_method :translate_no_cache, :translate
|
alias_method :translate_no_cache, :translate
|
||||||
|
alias_method :exists_no_cache?, :exists?
|
||||||
alias_method :reload_no_cache!, :reload!
|
alias_method :reload_no_cache!, :reload!
|
||||||
LRU_CACHE_SIZE = 300
|
LRU_CACHE_SIZE = 300
|
||||||
|
|
||||||
|
def init_accelerator!
|
||||||
|
@overrides_enabled = true
|
||||||
|
reload!
|
||||||
|
end
|
||||||
|
|
||||||
def reload!
|
def reload!
|
||||||
@loaded_locales = []
|
@loaded_locales = []
|
||||||
@cache = nil
|
@cache = nil
|
||||||
|
|
||||||
@overrides_enabled = true
|
|
||||||
@overrides_by_site = {}
|
@overrides_by_site = {}
|
||||||
|
|
||||||
reload_no_cache!
|
reload_no_cache!
|
||||||
@ -47,6 +51,21 @@ module I18n
|
|||||||
backend.fallbacks(locale).each {|l| ensure_loaded!(l) }
|
backend.fallbacks(locale).each {|l| ensure_loaded!(l) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def search(query, opts=nil)
|
||||||
|
load_locale(config.locale) unless @loaded_locales.include?(config.locale)
|
||||||
|
opts ||= {}
|
||||||
|
|
||||||
|
target = opts[:backend] || backend
|
||||||
|
results = target.search(config.locale, query)
|
||||||
|
|
||||||
|
regexp = /#{query}/i
|
||||||
|
(overrides_by_locale || {}).each do |k, v|
|
||||||
|
results.delete(k)
|
||||||
|
results[k] = v if (k =~ regexp || v =~ regexp)
|
||||||
|
end
|
||||||
|
results
|
||||||
|
end
|
||||||
|
|
||||||
def ensure_loaded!(locale)
|
def ensure_loaded!(locale)
|
||||||
@loaded_locales ||= []
|
@loaded_locales ||= []
|
||||||
load_locale(locale) unless @loaded_locales.include?(locale)
|
load_locale(locale) unless @loaded_locales.include?(locale)
|
||||||
@ -94,7 +113,7 @@ module I18n
|
|||||||
end
|
end
|
||||||
|
|
||||||
def client_overrides_json
|
def client_overrides_json
|
||||||
client_json = (overrides_by_locale || {}).select {|k, _| k.starts_with?('js.')}
|
client_json = (overrides_by_locale || {}).select {|k, _| k.starts_with?('js.') || k.starts_with?('admin_js.')}
|
||||||
MultiJson.dump(client_json)
|
MultiJson.dump(client_json)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -118,5 +137,11 @@ module I18n
|
|||||||
end
|
end
|
||||||
|
|
||||||
alias_method :t, :translate
|
alias_method :t, :translate
|
||||||
|
|
||||||
|
def exists?(*args)
|
||||||
|
load_locale(config.locale) unless @loaded_locales.include?(config.locale)
|
||||||
|
exists_no_cache?(*args)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -41,7 +41,25 @@ module I18n
|
|||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def search(locale, query)
|
||||||
|
find_results(/#{query}/i, {}, translations[locale])
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
def find_results(regexp, results, translations, path=nil)
|
||||||
|
return results if translations.blank?
|
||||||
|
|
||||||
|
translations.each do |k_sym, v|
|
||||||
|
k = k_sym.to_s
|
||||||
|
key_path = path ? "#{path}.#{k}" : k
|
||||||
|
if v.is_a?(String)
|
||||||
|
results[key_path] = v if key_path =~ regexp || v =~ regexp
|
||||||
|
elsif v.is_a?(Hash)
|
||||||
|
find_results(regexp, results, v, key_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
results
|
||||||
|
end
|
||||||
|
|
||||||
def lookup(locale, key, scope = [], options = {})
|
def lookup(locale, key, scope = [], options = {})
|
||||||
# Support interpolation and pluralization of overrides
|
# Support interpolation and pluralization of overrides
|
||||||
|
@ -371,7 +371,6 @@ module SiteSettingExtension
|
|||||||
protected
|
protected
|
||||||
|
|
||||||
def clear_cache!
|
def clear_cache!
|
||||||
SiteText.text_for_cache.clear
|
|
||||||
Rails.cache.delete(SiteSettingExtension.client_settings_cache_key)
|
Rails.cache.delete(SiteSettingExtension.client_settings_cache_key)
|
||||||
Site.clear_anon_cache!
|
Site.clear_anon_cache!
|
||||||
end
|
end
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
module SiteTextClassMethods
|
|
||||||
|
|
||||||
def text_types
|
|
||||||
@types || []
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_text_type(ct)
|
|
||||||
SiteText.text_types.find {|t| t.text_type == ct.to_sym}
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_text_type(text_type, opts=nil)
|
|
||||||
opts ||= {}
|
|
||||||
@types ||= []
|
|
||||||
format = opts[:format] || :markdown
|
|
||||||
@types << SiteTextType.new(text_type, format, opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
def text_for_cache
|
|
||||||
@text_for_cache ||= DistributedCache.new("text_for_cache")
|
|
||||||
end
|
|
||||||
|
|
||||||
def text_for(text_type, replacements=nil)
|
|
||||||
text = nil
|
|
||||||
text = text_for_cache[text_type] if replacements.blank?
|
|
||||||
text ||= uncached_text_for(text_type, replacements)
|
|
||||||
end
|
|
||||||
|
|
||||||
def uncached_text_for(text_type, replacements=nil)
|
|
||||||
store_cache = replacements.blank?
|
|
||||||
|
|
||||||
replacements ||= {}
|
|
||||||
replacements = {site_name: SiteSetting.title}.merge!(replacements)
|
|
||||||
replacements = SiteSetting.settings_hash.merge!(replacements)
|
|
||||||
|
|
||||||
site_text = SiteText.select(:value).find_by(text_type: text_type)
|
|
||||||
|
|
||||||
result = ""
|
|
||||||
if site_text.blank?
|
|
||||||
ct = find_text_type(text_type)
|
|
||||||
result = ct.default_text.dup if ct.present?
|
|
||||||
else
|
|
||||||
result = site_text.value.dup
|
|
||||||
end
|
|
||||||
|
|
||||||
result.gsub!(/\%\{[^}]+\}/) do |m|
|
|
||||||
replacements[m[2..-2].to_sym] || m
|
|
||||||
end
|
|
||||||
|
|
||||||
if store_cache
|
|
||||||
result.freeze
|
|
||||||
text_for_cache[text_type] = result
|
|
||||||
end
|
|
||||||
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_or_new(text_type)
|
|
||||||
site_text = SiteText.find_by(text_type: text_type)
|
|
||||||
return site_text if site_text.present?
|
|
||||||
|
|
||||||
site_text = SiteText.new
|
|
||||||
site_text.text_type = text_type
|
|
||||||
site_text
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
@ -50,7 +50,7 @@ class SystemMessage
|
|||||||
site_name: SiteSetting.title,
|
site_name: SiteSetting.title,
|
||||||
username: @recipient.username,
|
username: @recipient.username,
|
||||||
user_preferences_url: "#{Discourse.base_url}/users/#{@recipient.username_lower}/preferences",
|
user_preferences_url: "#{Discourse.base_url}/users/#{@recipient.username_lower}/preferences",
|
||||||
new_user_tips: SiteText.text_for(:usage_tips, base_url: Discourse.base_url),
|
new_user_tips: I18n.t('system_messages.usage_tips.text_body_template', base_url: Discourse.base_url),
|
||||||
site_password: "",
|
site_password: "",
|
||||||
base_url: Discourse.base_url,
|
base_url: Discourse.base_url,
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,21 @@ describe I18n::Backend::DiscourseI18n do
|
|||||||
expect(backend.translate(:en, 'wat', count: 3)).to eq("Hello 3")
|
expect(backend.translate(:en, 'wat', count: 3)).to eq("Hello 3")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'can be searched by key or value' do
|
||||||
|
expect(backend.search(:en, 'fo')).to eq({'foo' => 'Foo in :en'})
|
||||||
|
expect(backend.search(:en, 'foo')).to eq({'foo' => 'Foo in :en' })
|
||||||
|
expect(backend.search(:en, 'Foo')).to eq({'foo' => 'Foo in :en' })
|
||||||
|
expect(backend.search(:en, 'hello')).to eq({'wat' => 'Hello %{count}' })
|
||||||
|
expect(backend.search(:en, 'items.one')).to eq({'items.one' => 'one item' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can return multiple results' do
|
||||||
|
results = backend.search(:en, 'item')
|
||||||
|
|
||||||
|
expect(results['items.one']).to eq('one item')
|
||||||
|
expect(results['items.other']).to eq('%{count} items')
|
||||||
|
end
|
||||||
|
|
||||||
describe '#exists?' do
|
describe '#exists?' do
|
||||||
it 'returns true when a key is given that exists' do
|
it 'returns true when a key is given that exists' do
|
||||||
expect(backend.exists?(:de, :bar)).to eq(true)
|
expect(backend.exists?(:de, :bar)).to eq(true)
|
||||||
@ -67,12 +82,21 @@ describe I18n::Backend::DiscourseI18n do
|
|||||||
expect(I18n.translate('foo')).to eq('new value')
|
expect(I18n.translate('foo')).to eq('new value')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "can be searched" do
|
||||||
|
TranslationOverride.upsert!('en', 'wat', 'Overwritten value')
|
||||||
|
expect(I18n.search('wat', backend: backend)).to eq({'wat' => 'Overwritten value'})
|
||||||
|
expect(I18n.search('Overwritten', backend: backend)).to eq({'wat' => 'Overwritten value'})
|
||||||
|
expect(I18n.search('Hello', backend: backend)).to eq({})
|
||||||
|
end
|
||||||
|
|
||||||
it 'supports disabling' do
|
it 'supports disabling' do
|
||||||
TranslationOverride.upsert!('en', 'foo', 'meep')
|
orig_title = I18n.t('title')
|
||||||
|
TranslationOverride.upsert!('en', 'title', 'overridden title')
|
||||||
|
|
||||||
I18n.overrides_disabled do
|
I18n.overrides_disabled do
|
||||||
expect(I18n.translate('foo')).to eq('meep')
|
expect(I18n.translate('title')).to eq(orig_title)
|
||||||
end
|
end
|
||||||
|
expect(I18n.translate('title')).to eq('overridden title')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'supports interpolation' do
|
it 'supports interpolation' do
|
||||||
@ -104,10 +128,12 @@ describe I18n::Backend::DiscourseI18n do
|
|||||||
|
|
||||||
it "returns client overrides" do
|
it "returns client overrides" do
|
||||||
TranslationOverride.upsert!('en', 'js.foo', 'bar')
|
TranslationOverride.upsert!('en', 'js.foo', 'bar')
|
||||||
|
TranslationOverride.upsert!('en', 'admin_js.beep', 'boop')
|
||||||
json = ::JSON.parse(I18n.client_overrides_json)
|
json = ::JSON.parse(I18n.client_overrides_json)
|
||||||
|
|
||||||
expect(json).to be_present
|
expect(json).to be_present
|
||||||
expect(json['js.foo']).to eq('bar')
|
expect(json['js.foo']).to eq('bar')
|
||||||
|
expect(json['admin_js.beep']).to eq('boop')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
describe Admin::SiteTextTypesController do
|
|
||||||
|
|
||||||
it "is a subclass of AdminController" do
|
|
||||||
expect(Admin::SiteTextTypesController < Admin::AdminController).to eq(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'while logged in as an admin' do
|
|
||||||
before do
|
|
||||||
@user = log_in(:admin)
|
|
||||||
end
|
|
||||||
|
|
||||||
context ' .index' do
|
|
||||||
it 'returns success' do
|
|
||||||
xhr :get, :index
|
|
||||||
expect(response).to be_success
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns JSON' do
|
|
||||||
xhr :get, :index
|
|
||||||
expect(::JSON.parse(response.body)).to be_present
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
@ -11,17 +11,69 @@ describe Admin::SiteTextsController do
|
|||||||
@user = log_in(:admin)
|
@user = log_in(:admin)
|
||||||
end
|
end
|
||||||
|
|
||||||
context '.show' do
|
context '.index' do
|
||||||
let(:text_type) { SiteText.text_types.first.text_type }
|
it 'returns json' do
|
||||||
|
xhr :get, :index, q: 'title'
|
||||||
it 'returns success' do
|
|
||||||
xhr :get, :show, id: text_type
|
|
||||||
expect(response).to be_success
|
expect(response).to be_success
|
||||||
|
expect(::JSON.parse(response.body)).to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context '.show' do
|
||||||
|
it 'returns a site text for a key that exists' do
|
||||||
|
xhr :get, :show, id: 'title'
|
||||||
|
expect(response).to be_success
|
||||||
|
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json).to be_present
|
||||||
|
|
||||||
|
site_text = json['site_text']
|
||||||
|
expect(site_text).to be_present
|
||||||
|
|
||||||
|
expect(site_text['id']).to eq('title')
|
||||||
|
expect(site_text['value']).to eq(I18n.t(:title))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns JSON' do
|
it 'returns not found for missing keys' do
|
||||||
xhr :get, :show, id: text_type
|
xhr :get, :show, id: 'made_up_no_key_exists'
|
||||||
expect(::JSON.parse(response.body)).to be_present
|
expect(response).not_to be_success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context '.update and .revert' do
|
||||||
|
it 'updates and reverts the key' do
|
||||||
|
orig_title = I18n.t(:title)
|
||||||
|
|
||||||
|
xhr :put, :update, id: 'title', site_text: {value: 'hello'}
|
||||||
|
expect(response).to be_success
|
||||||
|
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json).to be_present
|
||||||
|
|
||||||
|
site_text = json['site_text']
|
||||||
|
expect(site_text).to be_present
|
||||||
|
|
||||||
|
expect(site_text['id']).to eq('title')
|
||||||
|
expect(site_text['value']).to eq('hello')
|
||||||
|
|
||||||
|
|
||||||
|
# Revert
|
||||||
|
xhr :put, :revert, id: 'title'
|
||||||
|
expect(response).to be_success
|
||||||
|
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json).to be_present
|
||||||
|
|
||||||
|
site_text = json['site_text']
|
||||||
|
expect(site_text).to be_present
|
||||||
|
|
||||||
|
expect(site_text['id']).to eq('title')
|
||||||
|
expect(site_text['value']).to eq(orig_title)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns not found for missing keys' do
|
||||||
|
xhr :put, :update, id: 'made_up_no_key_exists', site_text: {value: 'hello'}
|
||||||
|
expect(response).not_to be_success
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
Fabricator(:site_text) do
|
|
||||||
text_type "great.poem"
|
|
||||||
value "%{flower} are red. %{food} are blue."
|
|
||||||
end
|
|
||||||
|
|
||||||
Fabricator(:site_text_basic, from: :site_text) do
|
|
||||||
text_type "breaking.bad"
|
|
||||||
value "best show ever"
|
|
||||||
end
|
|
||||||
|
|
||||||
Fabricator(:site_text_site_setting, from: :site_text) do
|
|
||||||
text_type "site.replacement"
|
|
||||||
value "%{title} is evil."
|
|
||||||
end
|
|
@ -1,83 +0,0 @@
|
|||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
describe SiteText do
|
|
||||||
|
|
||||||
it { is_expected.to validate_presence_of :value }
|
|
||||||
|
|
||||||
|
|
||||||
describe "#text_for" do
|
|
||||||
|
|
||||||
it "returns an empty string for a missing text_type" do
|
|
||||||
expect(SiteText.text_for('something_random')).to eq("")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns the default value for a text` type with a default" do
|
|
||||||
expect(SiteText.text_for("usage_tips")).to be_present
|
|
||||||
end
|
|
||||||
|
|
||||||
it "correctly expires and bypasses cache" do
|
|
||||||
SiteSetting.enable_sso = false
|
|
||||||
text = SiteText.create!(text_type: "got.sso", value: "got sso: %{enable_sso}")
|
|
||||||
expect(SiteText.text_for("got.sso")).to eq("got sso: false")
|
|
||||||
SiteText.text_for("got.sso").frozen? == true
|
|
||||||
|
|
||||||
SiteSetting.enable_sso = true
|
|
||||||
wait_for do
|
|
||||||
SiteText.text_for("got.sso") == "got sso: true"
|
|
||||||
end
|
|
||||||
|
|
||||||
text.value = "I gots sso: %{enable_sso}"
|
|
||||||
text.save!
|
|
||||||
|
|
||||||
wait_for do
|
|
||||||
SiteText.text_for("got.sso") == "I gots sso: true"
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(SiteText.text_for("got.sso", enable_sso: "frog")).to eq("I gots sso: frog")
|
|
||||||
end
|
|
||||||
|
|
||||||
context "without replacements" do
|
|
||||||
let!(:site_text) { Fabricate(:site_text_basic) }
|
|
||||||
|
|
||||||
it "returns the simple string" do
|
|
||||||
expect(SiteText.text_for('breaking.bad')).to eq("best show ever")
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
context "with replacements" do
|
|
||||||
let!(:site_text) { Fabricate(:site_text) }
|
|
||||||
let(:replacements) { {flower: 'roses', food: 'grapes'} }
|
|
||||||
|
|
||||||
it "returns the correct string with replacements" do
|
|
||||||
expect(SiteText.text_for('great.poem', replacements)).to eq("roses are red. grapes are blue.")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "doesn't mind extra keys in the replacements" do
|
|
||||||
expect(SiteText.text_for('great.poem', replacements.merge(extra: 'key'))).to eq("roses are red. grapes are blue.")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "ignores missing keys" do
|
|
||||||
expect(SiteText.text_for('great.poem', flower: 'roses')).to eq("roses are red. %{food} are blue.")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
context "replacing site_settings" do
|
|
||||||
let!(:site_text) { Fabricate(:site_text_site_setting) }
|
|
||||||
|
|
||||||
it "replaces site_settings by default" do
|
|
||||||
SiteSetting.title = "Evil Trout"
|
|
||||||
expect(SiteText.text_for('site.replacement')).to eq("Evil Trout is evil.")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "allows us to override the default site settings" do
|
|
||||||
SiteSetting.title = "Evil Trout"
|
|
||||||
expect(SiteText.text_for('site.replacement', title: 'Good Tuna')).to eq("Good Tuna is evil.")
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
41
test/javascripts/acceptance/admin-site-text-test.js.es6
Normal file
41
test/javascripts/acceptance/admin-site-text-test.js.es6
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { acceptance } from "helpers/qunit-helpers";
|
||||||
|
|
||||||
|
acceptance("Admin - Site Texts", { loggedIn: true });
|
||||||
|
|
||||||
|
test("search for a key", () => {
|
||||||
|
visit("/admin/customize/site_texts");
|
||||||
|
|
||||||
|
fillIn('.site-text-search', 'Test');
|
||||||
|
andThen(() => ok(exists('.site-text')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("edit and revert a site text by key", () => {
|
||||||
|
visit("/admin/customize/site_texts/site.test");
|
||||||
|
andThen(() => {
|
||||||
|
equal(find('.title h3').text(), 'site.test');
|
||||||
|
ok(!exists('.save-messages .saved'));
|
||||||
|
ok(!exists('.save-messages .saved'));
|
||||||
|
ok(!exists('.revert-site-text'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change the value
|
||||||
|
fillIn('.site-text-value', 'New Test Value');
|
||||||
|
click(".save-changes");
|
||||||
|
|
||||||
|
andThen(() => {
|
||||||
|
ok(exists('.save-messages .saved'));
|
||||||
|
ok(exists('.revert-site-text'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Revert the changes
|
||||||
|
click('.revert-site-text');
|
||||||
|
andThen(() => {
|
||||||
|
ok(exists('.bootbox.modal'));
|
||||||
|
});
|
||||||
|
click('.bootbox.modal .btn-primary');
|
||||||
|
|
||||||
|
andThen(() => {
|
||||||
|
ok(!exists('.save-messages .saved'));
|
||||||
|
ok(!exists('.revert-site-text'));
|
||||||
|
});
|
||||||
|
});
|
@ -1,75 +0,0 @@
|
|||||||
import { acceptance } from "helpers/qunit-helpers";
|
|
||||||
|
|
||||||
acceptance("Queued Posts", { loggedIn: true });
|
|
||||||
|
|
||||||
test("approve a post", () => {
|
|
||||||
visit("/queued-posts");
|
|
||||||
|
|
||||||
click('.queued-post:eq(0) button.approve');
|
|
||||||
andThen(() => {
|
|
||||||
ok(!exists('.queued-post'), 'it removes the post');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("reject a post", () => {
|
|
||||||
visit("/queued-posts");
|
|
||||||
|
|
||||||
click('.queued-post:eq(0) button.reject');
|
|
||||||
andThen(() => {
|
|
||||||
ok(!exists('.queued-post'), 'it removes the post');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("delete user", () => {
|
|
||||||
visit("/queued-posts");
|
|
||||||
|
|
||||||
click('.queued-post:eq(0) button.delete-user');
|
|
||||||
andThen(() => {
|
|
||||||
ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog');
|
|
||||||
});
|
|
||||||
|
|
||||||
click('.modal-footer a:eq(1)');
|
|
||||||
andThen(() => {
|
|
||||||
ok(!exists('.bootbox.modal'), 'it dismisses the modal');
|
|
||||||
ok(exists('.queued-post'), "it doesn't remove the post");
|
|
||||||
});
|
|
||||||
|
|
||||||
click('.queued-post:eq(0) button.delete-user');
|
|
||||||
click('.modal-footer a:eq(0)');
|
|
||||||
andThen(() => {
|
|
||||||
ok(!exists('.bootbox.modal'), 'it dismisses the modal');
|
|
||||||
ok(!exists('.queued-post'), "it removes the post");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("edit a post - cancel", () => {
|
|
||||||
visit("/queued-posts");
|
|
||||||
|
|
||||||
click('.queued-post:eq(0) button.edit');
|
|
||||||
andThen(() => {
|
|
||||||
equal(find('.queued-post:eq(0) textarea').val(), 'queued post text', 'it shows an editor');
|
|
||||||
});
|
|
||||||
|
|
||||||
fillIn('.queued-post:eq(0) textarea', 'new post text');
|
|
||||||
click('.queued-post:eq(0) button.cancel');
|
|
||||||
andThen(() => {
|
|
||||||
ok(!exists('textarea'), 'it disables editing');
|
|
||||||
equal(find('.queued-post:eq(0) .body p').text(), 'queued post text', 'it reverts the new text');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("edit a post - confirm", () => {
|
|
||||||
visit("/queued-posts");
|
|
||||||
|
|
||||||
click('.queued-post:eq(0) button.edit');
|
|
||||||
andThen(() => {
|
|
||||||
equal(find('.queued-post:eq(0) textarea').val(), 'queued post text', 'it shows an editor');
|
|
||||||
});
|
|
||||||
|
|
||||||
fillIn('.queued-post:eq(0) textarea', 'new post text');
|
|
||||||
click('.queued-post:eq(0) button.confirm');
|
|
||||||
andThen(() => {
|
|
||||||
ok(!exists('textarea'), 'it disables editing');
|
|
||||||
equal(find('.queued-post:eq(0) .body p').text(), 'new post text', 'it has the new text');
|
|
||||||
});
|
|
||||||
});
|
|
@ -25,9 +25,7 @@ function response(code, obj) {
|
|||||||
return [code, {"Content-Type": "application/json"}, obj];
|
return [code, {"Content-Type": "application/json"}, obj];
|
||||||
}
|
}
|
||||||
|
|
||||||
function success() {
|
const success = () => response({ success: true });
|
||||||
return response({ success: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const _widgets = [
|
const _widgets = [
|
||||||
{id: 123, name: 'Trout Lure'},
|
{id: 123, name: 'Trout Lure'},
|
||||||
@ -50,9 +48,7 @@ const colors = [{id: 1, name: 'Red'},
|
|||||||
{id: 2, name: 'Green'},
|
{id: 2, name: 'Green'},
|
||||||
{id: 3, name: 'Yellow'}];
|
{id: 3, name: 'Yellow'}];
|
||||||
|
|
||||||
function loggedIn() {
|
const loggedIn = () => !!Discourse.User.current();
|
||||||
return !!Discourse.User.current();
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function() {
|
export default function() {
|
||||||
|
|
||||||
@ -77,9 +73,9 @@ export default function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.get('/admin/plugins', () => { return response({ plugins: [] }); });
|
this.get('/admin/plugins', () => response({ plugins: [] }));
|
||||||
|
|
||||||
this.get('/composer-messages', () => { return response([]); });
|
this.get('/composer-messages', () => response([]));
|
||||||
|
|
||||||
this.get("/latest.json", () => {
|
this.get("/latest.json", () => {
|
||||||
const json = fixturesByUrl['/latest.json'];
|
const json = fixturesByUrl['/latest.json'];
|
||||||
@ -101,27 +97,19 @@ export default function() {
|
|||||||
return response(json);
|
return response(json);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.put('/users/eviltrout', () => {
|
this.put('/users/eviltrout', () => response({ user: {} }));
|
||||||
return response({ user: {} });
|
|
||||||
});
|
|
||||||
|
|
||||||
this.get("/t/280.json", function() {
|
this.get("/t/280.json", () => response(fixturesByUrl['/t/280/1.json']));
|
||||||
return response(fixturesByUrl['/t/280/1.json']);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.get("/t/28830.json", function() {
|
this.get("/t/28830.json", () => response(fixturesByUrl['/t/28830/1.json']));
|
||||||
return response(fixturesByUrl['/t/28830/1.json']);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.get("/t/9.json", function() {
|
this.get("/t/9.json", () => response(fixturesByUrl['/t/9/1.json']));
|
||||||
return response(fixturesByUrl['/t/9/1.json']);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.get("/t/id_for/:slug", function() {
|
this.get("/t/id_for/:slug", () => {
|
||||||
return response({id: 280, slug: "internationalization-localization", url: "/t/internationalization-localization/280"});
|
return response({id: 280, slug: "internationalization-localization", url: "/t/internationalization-localization/280"});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.get("/404-body", function() {
|
this.get("/404-body", () => {
|
||||||
return [200, {"Content-Type": "text/html"}, "<div class='page-not-found'>not found</div>"];
|
return [200, {"Content-Type": "text/html"}, "<div class='page-not-found'>not found</div>"];
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -135,9 +123,7 @@ export default function() {
|
|||||||
return response({category});
|
return response({category});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.get('/draft.json', function() {
|
this.get('/draft.json', () => response({}));
|
||||||
return response({});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.put('/queued_posts/:queued_post_id', function(request) {
|
this.put('/queued_posts/:queued_post_id', function(request) {
|
||||||
return response({ queued_post: {id: request.params.queued_post_id } });
|
return response({ queued_post: {id: request.params.queued_post_id } });
|
||||||
@ -173,37 +159,25 @@ export default function() {
|
|||||||
return response({available: true});
|
return response({available: true});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.post('/users', function() {
|
this.post('/users', () => response({success: true}));
|
||||||
return response({success: true});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.get('/login.html', function() {
|
this.get('/login.html', () => [200, {}, 'LOGIN PAGE']);
|
||||||
return [200, {}, 'LOGIN PAGE'];
|
|
||||||
});
|
|
||||||
|
|
||||||
this.delete('/posts/:post_id', success);
|
this.delete('/posts/:post_id', success);
|
||||||
this.put('/posts/:post_id/recover', success);
|
this.put('/posts/:post_id/recover', success);
|
||||||
|
|
||||||
this.put('/posts/:post_id', (request) => {
|
this.put('/posts/:post_id', request => {
|
||||||
const data = parsePostData(request.requestBody);
|
const data = parsePostData(request.requestBody);
|
||||||
data.post.id = request.params.post_id;
|
data.post.id = request.params.post_id;
|
||||||
data.post.version = 2;
|
data.post.version = 2;
|
||||||
return response(200, data.post);
|
return response(200, data.post);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.get('/t/403.json', () => {
|
this.get('/t/403.json', () => response(403, {}));
|
||||||
return response(403, {});
|
this.get('/t/404.json', () => response(404, "not found"));
|
||||||
});
|
this.get('/t/500.json', () => response(502, {}));
|
||||||
|
|
||||||
this.get('/t/404.json', () => {
|
this.put('/t/:slug/:id', request => {
|
||||||
return response(404, "not found");
|
|
||||||
});
|
|
||||||
|
|
||||||
this.get('/t/500.json', () => {
|
|
||||||
return response(502, {});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.put('/t/:slug/:id', (request) => {
|
|
||||||
const data = parsePostData(request.requestBody);
|
const data = parsePostData(request.requestBody);
|
||||||
|
|
||||||
return response(200, { basic_topic: {id: request.params.id,
|
return response(200, { basic_topic: {id: request.params.id,
|
||||||
@ -264,7 +238,6 @@ export default function() {
|
|||||||
return response({ cool_thing });
|
return response({ cool_thing });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this.get('/widgets', function(request) {
|
this.get('/widgets', function(request) {
|
||||||
let result = _widgets;
|
let result = _widgets;
|
||||||
|
|
||||||
@ -286,8 +259,18 @@ export default function() {
|
|||||||
|
|
||||||
this.delete('/widgets/:widget_id', success);
|
this.delete('/widgets/:widget_id', success);
|
||||||
|
|
||||||
this.post('/topics/timings', function() {
|
this.post('/topics/timings', () => response(200, {}));
|
||||||
return response(200, {});
|
|
||||||
|
const siteText = {id: 'site.test', value: 'Test McTest'};
|
||||||
|
this.get('/admin/customize/site_texts', () => response(200, {site_texts: [siteText] }));
|
||||||
|
this.get('/admin/customize/site_texts/:key', () => response(200, {site_text: siteText }));
|
||||||
|
this.delete('/admin/customize/site_texts/:key', () => response(200, {site_text: siteText }));
|
||||||
|
|
||||||
|
this.put('/admin/customize/site_texts/:key', request => {
|
||||||
|
const result = parsePostData(request.requestBody);
|
||||||
|
result.id = request.params.key;
|
||||||
|
result.can_revert = true;
|
||||||
|
return response(200, {site_text: result});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -304,9 +287,6 @@ export default function() {
|
|||||||
throw error;
|
throw error;
|
||||||
};
|
};
|
||||||
|
|
||||||
server.checkPassthrough = function(request) {
|
server.checkPassthrough = request => request.requestHeaders['Discourse-Script'];
|
||||||
return request.requestHeaders['Discourse-Script'];
|
|
||||||
};
|
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user