mirror of
https://github.com/discourse/discourse.git
synced 2025-01-22 13:37:47 +08:00
Work in Progress: Content Editing in Admin Section
This commit is contained in:
parent
bd0e98aec2
commit
fa1ba6791b
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
This controller is used for editing site content
|
||||||
|
|
||||||
|
@class AdminSiteContentEditController
|
||||||
|
@extends Ember.ObjectController
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.AdminSiteContentEditController = Discourse.ObjectController.extend({
|
||||||
|
|
||||||
|
saveDisabled: function() {
|
||||||
|
if (this.get('saving')) return true;
|
||||||
|
if (this.blank('content.content')) return true;
|
||||||
|
return false;
|
||||||
|
}.property('saving', 'content.content'),
|
||||||
|
|
||||||
|
saveChanges: function() {
|
||||||
|
var controller = this;
|
||||||
|
controller.setProperties({saving: true, saved: false});
|
||||||
|
this.get('content').save().then(function () {
|
||||||
|
controller.setProperties({saving: false, saved: true});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
39
app/assets/javascripts/admin/models/site_content.js
Normal file
39
app/assets/javascripts/admin/models/site_content.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
Our data model for interacting with custom site content
|
||||||
|
|
||||||
|
@class SiteContent
|
||||||
|
@extends Discourse.Model
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.SiteContent = Discourse.Model.extend({
|
||||||
|
|
||||||
|
markdown: Ember.computed.equal('format', 'markdown'),
|
||||||
|
plainText: Ember.computed.equal('format', 'plain'),
|
||||||
|
html: Ember.computed.equal('format', 'html'),
|
||||||
|
css: Ember.computed.equal('format', 'css'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
Save the content
|
||||||
|
|
||||||
|
@method save
|
||||||
|
@return {jqXHR} a jQuery Promise object
|
||||||
|
**/
|
||||||
|
save: function() {
|
||||||
|
return Discourse.ajax(Discourse.getURL("/admin/site_contents/" + this.get('content_type')), {
|
||||||
|
type: 'PUT',
|
||||||
|
data: {content: this.get('content')}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Discourse.SiteContent.reopenClass({
|
||||||
|
|
||||||
|
find: function(type) {
|
||||||
|
return Discourse.ajax(Discourse.getURL("/admin/site_contents/" + type)).then(function (data) {
|
||||||
|
return Discourse.SiteContent.create(data.site_content);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
21
app/assets/javascripts/admin/models/site_content_type.js
Normal file
21
app/assets/javascripts/admin/models/site_content_type.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
Our data model that represents types of editing site content
|
||||||
|
|
||||||
|
@class SiteContentType
|
||||||
|
@extends Discourse.Model
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.SiteContentType = Discourse.Model.extend({});
|
||||||
|
|
||||||
|
Discourse.SiteContentType.reopenClass({
|
||||||
|
findAll: function() {
|
||||||
|
var contentTypes = Em.A();
|
||||||
|
Discourse.ajax(Discourse.getURL("/admin/site_content_types")).then(function(data) {
|
||||||
|
data.forEach(function (ct) {
|
||||||
|
contentTypes.pushObject(Discourse.SiteContentType.create(ct));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return contentTypes;
|
||||||
|
}
|
||||||
|
});
|
|
@ -14,19 +14,17 @@ Discourse.SiteCustomization = Discourse.Model.extend({
|
||||||
return this.startTrackingChanges();
|
return this.startTrackingChanges();
|
||||||
},
|
},
|
||||||
|
|
||||||
description: (function() {
|
description: function() {
|
||||||
return "" + this.name + (this.enabled ? ' (*)' : '');
|
return "" + this.name + (this.enabled ? ' (*)' : '');
|
||||||
}).property('selected', 'name'),
|
}.property('selected', 'name'),
|
||||||
|
|
||||||
changed: (function() {
|
changed: function() {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
if (!this.originals) {
|
if (!this.originals) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.trackedProperties.any(function(p) {
|
return this.trackedProperties.any(function(p) {
|
||||||
return _this.originals[p] !== _this.get(p);
|
return _this.originals[p] !== _this.get(p);
|
||||||
});
|
});
|
||||||
}).property('override_default_style', 'enabled', 'name', 'stylesheet', 'header', 'originals'),
|
}.property('override_default_style', 'enabled', 'name', 'stylesheet', 'header', 'originals'),
|
||||||
|
|
||||||
startTrackingChanges: function() {
|
startTrackingChanges: function() {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
|
@ -37,18 +35,17 @@ Discourse.SiteCustomization = Discourse.Model.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
previewUrl: (function() {
|
previewUrl: function() {
|
||||||
return "/?preview-style=" + (this.get('key'));
|
return "/?preview-style=" + (this.get('key'));
|
||||||
}).property('key'),
|
}.property('key'),
|
||||||
|
|
||||||
disableSave: (function() {
|
disableSave: function() {
|
||||||
return !this.get('changed');
|
return !this.get('changed');
|
||||||
}).property('changed'),
|
}.property('changed'),
|
||||||
|
|
||||||
save: function() {
|
save: function() {
|
||||||
var data;
|
|
||||||
this.startTrackingChanges();
|
this.startTrackingChanges();
|
||||||
data = {
|
var data = {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
enabled: this.enabled,
|
enabled: this.enabled,
|
||||||
stylesheet: this.stylesheet,
|
stylesheet: this.stylesheet,
|
||||||
|
@ -66,7 +63,6 @@ Discourse.SiteCustomization = Discourse.Model.extend({
|
||||||
|
|
||||||
destroy: function() {
|
destroy: function() {
|
||||||
if (!this.id) return;
|
if (!this.id) return;
|
||||||
|
|
||||||
return Discourse.ajax({
|
return Discourse.ajax({
|
||||||
url: Discourse.getURL("/admin/site_customizations/") + this.id,
|
url: Discourse.getURL("/admin/site_customizations/") + this.id,
|
||||||
type: 'DELETE'
|
type: 'DELETE'
|
||||||
|
@ -76,13 +72,12 @@ Discourse.SiteCustomization = Discourse.Model.extend({
|
||||||
});
|
});
|
||||||
|
|
||||||
var SiteCustomizations = Ember.ArrayProxy.extend({
|
var SiteCustomizations = Ember.ArrayProxy.extend({
|
||||||
selectedItemChanged: (function() {
|
selectedItemChanged: function() {
|
||||||
var selected;
|
var selected = this.get('selectedItem');
|
||||||
selected = this.get('selectedItem');
|
|
||||||
return this.get('content').each(function(i) {
|
return this.get('content').each(function(i) {
|
||||||
return i.set('selected', selected === i);
|
return i.set('selected', selected === i);
|
||||||
});
|
});
|
||||||
}).observes('selectedItem')
|
}.observes('selectedItem')
|
||||||
});
|
});
|
||||||
|
|
||||||
Discourse.SiteCustomization.reopenClass({
|
Discourse.SiteCustomization.reopenClass({
|
||||||
|
|
|
@ -8,6 +8,12 @@ Discourse.Route.buildRoutes(function() {
|
||||||
this.resource('admin', { path: '/admin' }, function() {
|
this.resource('admin', { path: '/admin' }, function() {
|
||||||
this.route('dashboard', { path: '/' });
|
this.route('dashboard', { path: '/' });
|
||||||
this.route('site_settings', { path: '/site_settings' });
|
this.route('site_settings', { path: '/site_settings' });
|
||||||
|
|
||||||
|
|
||||||
|
this.resource('adminSiteContents', { path: '/site_contents' }, function() {
|
||||||
|
this.resource('adminSiteContentEdit', {path: '/:content_type'});
|
||||||
|
});
|
||||||
|
|
||||||
this.route('email_logs', { path: '/email_logs' });
|
this.route('email_logs', { path: '/email_logs' });
|
||||||
this.route('customize', { path: '/customize' });
|
this.route('customize', { path: '/customize' });
|
||||||
this.route('api', {path: '/api'});
|
this.route('api', {path: '/api'});
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
Allows users to customize site content
|
||||||
|
|
||||||
|
@class AdminSiteContentEditRoute
|
||||||
|
@extends Discourse.Route
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.AdminSiteContentEditRoute = Discourse.Route.extend({
|
||||||
|
|
||||||
|
serialize: function(model) {
|
||||||
|
return {content_type: model.get('content_type')};
|
||||||
|
},
|
||||||
|
|
||||||
|
model: function(params) {
|
||||||
|
return {content_type: params.content_type};
|
||||||
|
},
|
||||||
|
|
||||||
|
renderTemplate: function() {
|
||||||
|
this.render('admin/templates/site_content_edit', {into: 'admin/templates/site_contents'});
|
||||||
|
},
|
||||||
|
|
||||||
|
exit: function() {
|
||||||
|
this._super();
|
||||||
|
this.render('admin/templates/site_contents_empty', {into: 'admin/templates/site_contents'});
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController: function(controller, model) {
|
||||||
|
controller.set('loaded', false);
|
||||||
|
controller.setProperties({saving: false, saved: false});
|
||||||
|
|
||||||
|
Discourse.SiteContent.find(Em.get(model, 'content_type')).then(function (sc) {
|
||||||
|
controller.set('content', sc);
|
||||||
|
controller.set('loaded', true);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
Allows users to customize site content
|
||||||
|
|
||||||
|
@class AdminSiteContentsRoute
|
||||||
|
@extends Discourse.Route
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.AdminSiteContentsRoute = Discourse.Route.extend({
|
||||||
|
|
||||||
|
model: function() {
|
||||||
|
return Discourse.SiteContentType.findAll();
|
||||||
|
},
|
||||||
|
|
||||||
|
renderTemplate: function() {
|
||||||
|
this.render('admin/templates/site_contents', {into: 'admin/templates/admin'});
|
||||||
|
this.render('admin/templates/site_contents_empty', {into: 'admin/templates/site_contents'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="full-width">
|
<div class="full-width">
|
||||||
|
|
||||||
<ul class="nav nav-pills">
|
<ul class="nav nav-pills">
|
||||||
<li>{{#linkTo 'admin.dashboard'}}{{i18n admin.dashboard.title}}{{/linkTo}}</li>
|
<li>{{#linkTo 'admin.dashboard'}}{{i18n admin.dashboard.title}}{{/linkTo}}</li>
|
||||||
<li>{{#linkTo 'admin.site_settings'}}{{i18n admin.site_settings.title}}{{/linkTo}}</li>
|
<li>{{#linkTo 'admin.site_settings'}}{{i18n admin.site_settings.title}}{{/linkTo}}</li>
|
||||||
|
<li>{{#linkTo 'adminSiteContents'}}{{i18n admin.site_content.title}}{{/linkTo}}</li>
|
||||||
<li>{{#linkTo 'adminUsersList.active'}}{{i18n admin.users.title}}{{/linkTo}}</li>
|
<li>{{#linkTo 'adminUsersList.active'}}{{i18n admin.users.title}}{{/linkTo}}</li>
|
||||||
<li>{{#linkTo 'admin.email_logs'}}{{i18n admin.email_logs.title}}{{/linkTo}}</li>
|
<li>{{#linkTo 'admin.email_logs'}}{{i18n admin.email_logs.title}}{{/linkTo}}</li>
|
||||||
<li>{{#linkTo 'adminFlags.active'}}{{i18n admin.flags.title}}{{/linkTo}}</li>
|
<li>{{#linkTo 'adminFlags.active'}}{{i18n admin.flags.title}}{{/linkTo}}</li>
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
{{#if loaded}}
|
||||||
|
<h3>{{title}}</h3>
|
||||||
|
<p class='description'>{{description}}</p>
|
||||||
|
|
||||||
|
{{#if markdown}}
|
||||||
|
{{view Discourse.PagedownEditor valueBinding="content.content"}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if plainText}}
|
||||||
|
{{view Ember.TextArea valueBinding="content.content" class="plain"}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if html}}
|
||||||
|
{{view Discourse.AceEditorView contentBinding="content.content" mode="html"}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if css}}
|
||||||
|
{{view Discourse.AceEditorView contentBinding="content.content" mode="css"}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class='controls'>
|
||||||
|
<button class='btn' {{action saveChanges}} {{bindAttr disabled="saveDisabled"}}>
|
||||||
|
{{#if saving}}
|
||||||
|
{{i18n saving}}
|
||||||
|
{{else}}
|
||||||
|
{{i18n save}}
|
||||||
|
{{/if}}
|
||||||
|
</button>
|
||||||
|
{{#if saved}}{{i18n saved}}{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{else}}
|
||||||
|
<div class='spinner'>{{i18n loading}}</div>
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,16 @@
|
||||||
|
<div class='row'>
|
||||||
|
<div class='content-list span6'>
|
||||||
|
<h3>{{i18n admin.site_content.edit}}</h3>
|
||||||
|
<ul>
|
||||||
|
{{#each type in content}}
|
||||||
|
<li>
|
||||||
|
{{#linkTo 'adminSiteContentEdit' type}}{{type.title}}{{/linkTo}}
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='content-editor span15'>
|
||||||
|
{{outlet}}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1 @@
|
||||||
|
<p>{{i18n admin.site_content.none}}</p>
|
|
@ -36,6 +36,7 @@ Discourse.AceEditorView = Discourse.View.extend({
|
||||||
didInsertElement: function() {
|
didInsertElement: function() {
|
||||||
var initAce,
|
var initAce,
|
||||||
_this = this;
|
_this = this;
|
||||||
|
|
||||||
initAce = function() {
|
initAce = function() {
|
||||||
_this.editor = ace.edit(_this.$('.ace')[0]);
|
_this.editor = ace.edit(_this.$('.ace')[0]);
|
||||||
_this.editor.setTheme("ace/theme/chrome");
|
_this.editor.setTheme("ace/theme/chrome");
|
||||||
|
|
|
@ -134,6 +134,10 @@ Discourse = Ember.Application.createWithMixins({
|
||||||
if (href === '#') return;
|
if (href === '#') return;
|
||||||
if ($currentTarget.attr('target')) return;
|
if ($currentTarget.attr('target')) return;
|
||||||
if ($currentTarget.data('auto-route')) return;
|
if ($currentTarget.data('auto-route')) return;
|
||||||
|
|
||||||
|
// If it's an ember #linkTo skip it
|
||||||
|
if ($currentTarget.hasClass('ember-view')) return;
|
||||||
|
|
||||||
if ($currentTarget.hasClass('lightbox')) return;
|
if ($currentTarget.hasClass('lightbox')) return;
|
||||||
if (href.indexOf("mailto:") === 0) return;
|
if (href.indexOf("mailto:") === 0) return;
|
||||||
if (href.match(/^http[s]?:\/\//i) && !href.match(new RegExp("^http:\\/\\/" + window.location.hostname, "i"))) return;
|
if (href.match(/^http[s]?:\/\//i) && !href.match(new RegExp("^http:\\/\\/" + window.location.hostname, "i"))) return;
|
||||||
|
|
|
@ -11,6 +11,7 @@ Discourse.PreferencesEmailController = Discourse.ObjectController.extend({
|
||||||
saving: false,
|
saving: false,
|
||||||
error: false,
|
error: false,
|
||||||
success: false,
|
success: false,
|
||||||
|
newEmail: null,
|
||||||
|
|
||||||
saveDisabled: (function() {
|
saveDisabled: (function() {
|
||||||
if (this.get('saving')) return true;
|
if (this.get('saving')) return true;
|
||||||
|
|
|
@ -73,8 +73,7 @@ Ember.Handlebars.registerBoundHelper('boundCategoryLink', function(category) {
|
||||||
@for Handlebars
|
@for Handlebars
|
||||||
**/
|
**/
|
||||||
Handlebars.registerHelper('titledLinkTo', function(name, object) {
|
Handlebars.registerHelper('titledLinkTo', function(name, object) {
|
||||||
var options;
|
var options = [].slice.call(arguments, -1)[0];
|
||||||
options = [].slice.call(arguments, -1)[0];
|
|
||||||
if (options.hash.titleKey) {
|
if (options.hash.titleKey) {
|
||||||
options.hash.title = Em.String.i18n(options.hash.titleKey);
|
options.hash.title = Em.String.i18n(options.hash.titleKey);
|
||||||
}
|
}
|
||||||
|
|
|
@ -345,12 +345,12 @@ Discourse.User = Discourse.Model.extend({
|
||||||
}).property('stats.@each'),
|
}).property('stats.@each'),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Number of items this user has sent.
|
Number of items this user has sent.
|
||||||
|
|
||||||
@property sentItemsCount
|
@property sentItemsCount
|
||||||
@type {Integer}
|
@type {Integer}
|
||||||
**/
|
**/
|
||||||
sentItemsCount: (function() {
|
sentItemsCount: function() {
|
||||||
var r;
|
var r;
|
||||||
r = 0;
|
r = 0;
|
||||||
this.get('stats').each(function(s) {
|
this.get('stats').each(function(s) {
|
||||||
|
@ -360,7 +360,42 @@ Discourse.User = Discourse.Model.extend({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return r;
|
return r;
|
||||||
}).property('stats.@each')
|
}.property('stats.@each'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
Load extra details for the user
|
||||||
|
|
||||||
|
@method loadDetails
|
||||||
|
**/
|
||||||
|
loadDetails: function() {
|
||||||
|
|
||||||
|
// Check the preload store first
|
||||||
|
var user = this;
|
||||||
|
var username = this.get('username');
|
||||||
|
PreloadStore.getAndRemove("user_" + username, function() {
|
||||||
|
return Discourse.ajax({ url: Discourse.getURL("/users/") + username + '.json' });
|
||||||
|
}).then(function (json) {
|
||||||
|
// Create a user from the resulting JSON
|
||||||
|
json.user.stats = Discourse.User.groupStats(json.user.stats.map(function(s) {
|
||||||
|
var stat = Em.Object.create(s);
|
||||||
|
stat.set('isPM', stat.get('action_type') === Discourse.UserAction.NEW_PRIVATE_MESSAGE ||
|
||||||
|
stat.get('action_type') === Discourse.UserAction.GOT_PRIVATE_MESSAGE);
|
||||||
|
return stat;
|
||||||
|
}));
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
if (json.user.stream) {
|
||||||
|
count = json.user.stream.length;
|
||||||
|
json.user.stream = Discourse.UserAction.collapseStream(json.user.stream.map(function(ua) {
|
||||||
|
return Discourse.UserAction.create(ua);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setProperties(json.user);
|
||||||
|
user.set('totalItems', count);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Discourse.User.reopenClass({
|
Discourse.User.reopenClass({
|
||||||
|
@ -427,42 +462,6 @@ Discourse.User.reopenClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
Finds a user based on a username
|
|
||||||
|
|
||||||
@method find
|
|
||||||
@param {String} username The username
|
|
||||||
@returns a promise that will resolve to the user
|
|
||||||
**/
|
|
||||||
find: function(username) {
|
|
||||||
|
|
||||||
// Check the preload store first
|
|
||||||
return PreloadStore.getAndRemove("user_" + username, function() {
|
|
||||||
return Discourse.ajax({ url: Discourse.getURL("/users/") + username + '.json' });
|
|
||||||
}).then(function (json) {
|
|
||||||
|
|
||||||
// Create a user from the resulting JSON
|
|
||||||
json.user.stats = Discourse.User.groupStats(json.user.stats.map(function(s) {
|
|
||||||
var stat = Em.Object.create(s);
|
|
||||||
stat.set('isPM', stat.get('action_type') === Discourse.UserAction.NEW_PRIVATE_MESSAGE ||
|
|
||||||
stat.get('action_type') === Discourse.UserAction.GOT_PRIVATE_MESSAGE);
|
|
||||||
return stat;
|
|
||||||
}));
|
|
||||||
|
|
||||||
var count = 0;
|
|
||||||
if (json.user.stream) {
|
|
||||||
count = json.user.stream.length;
|
|
||||||
json.user.stream = Discourse.UserAction.collapseStream(json.user.stream.map(function(ua) {
|
|
||||||
return Discourse.UserAction.create(ua);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = Discourse.User.create(json.user);
|
|
||||||
user.set('totalItems', count);
|
|
||||||
return user;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Creates a new account over POST
|
Creates a new account over POST
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,16 @@ Discourse.PreferencesEmailRoute = Discourse.RestrictedUserRoute.extend({
|
||||||
this.render({ into: 'user', outlet: 'userOutlet' });
|
this.render({ into: 'user', outlet: 'userOutlet' });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// A bit odd, but if we leave to /preferences we need to re-render that outlet
|
||||||
|
exit: function() {
|
||||||
|
this._super();
|
||||||
|
this.render('preferences', {
|
||||||
|
into: 'user',
|
||||||
|
outlet: 'userOutlet',
|
||||||
|
controller: 'preferences'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
setupController: function(controller) {
|
setupController: function(controller) {
|
||||||
controller.set('content', this.controllerFor('user').get('content'));
|
controller.set('content', this.controllerFor('user').get('content'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
setupController: function(controller) {
|
setupController: function(controller) {
|
||||||
|
console.log('prefereces');
|
||||||
controller.set('content', this.controllerFor('user').get('content'));
|
controller.set('content', this.controllerFor('user').get('content'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,16 @@ Discourse.PreferencesUsernameRoute = Discourse.RestrictedUserRoute.extend({
|
||||||
return this.render({ into: 'user', outlet: 'userOutlet' });
|
return this.render({ into: 'user', outlet: 'userOutlet' });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// A bit odd, but if we leave to /preferences we need to re-render that outlet
|
||||||
|
exit: function() {
|
||||||
|
this._super();
|
||||||
|
this.render('preferences', {
|
||||||
|
into: 'user',
|
||||||
|
outlet: 'userOutlet',
|
||||||
|
controller: 'preferences'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
setupController: function(controller) {
|
setupController: function(controller) {
|
||||||
var user = this.controllerFor('user').get('content');
|
var user = this.controllerFor('user').get('content');
|
||||||
controller.set('content', user);
|
controller.set('content', user);
|
||||||
|
|
|
@ -8,10 +8,15 @@
|
||||||
**/
|
**/
|
||||||
Discourse.UserRoute = Discourse.Route.extend({
|
Discourse.UserRoute = Discourse.Route.extend({
|
||||||
model: function(params) {
|
model: function(params) {
|
||||||
return Discourse.User.find(params.username);
|
return Discourse.User.create({username: params.username});
|
||||||
},
|
},
|
||||||
|
|
||||||
serialize: function(params) {
|
serialize: function(params) {
|
||||||
return { username: Em.get(params, 'username').toLowerCase() };
|
return { username: Em.get(params, 'username').toLowerCase() };
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController: function(controller, model) {
|
||||||
|
model.loadDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -101,7 +101,7 @@
|
||||||
<label>{{i18n user.new_topic_duration.label}}</label>
|
<label>{{i18n user.new_topic_duration.label}}</label>
|
||||||
{{view Discourse.ComboboxView valueAttribute="value" contentBinding="controller.considerNewTopicOptions" valueBinding="content.new_topic_duration_minutes"}}
|
{{view Discourse.ComboboxView valueAttribute="value" contentBinding="controller.considerNewTopicOptions" valueBinding="content.new_topic_duration_minutes"}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<label>{{view Ember.Checkbox checkedBinding="content.external_links_in_new_tab"}}
|
<label>{{view Ember.Checkbox checkedBinding="content.external_links_in_new_tab"}}
|
||||||
{{i18n user.external_links_in_new_tab}}</label>
|
{{i18n user.external_links_in_new_tab}}</label>
|
||||||
|
|
|
@ -565,4 +565,78 @@ table {
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
border-left: solid 1px #ddd;
|
border-left: solid 1px #ddd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-list {
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: $darkish_gray;
|
||||||
|
font-size: 15px;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
li a {
|
||||||
|
display: block;
|
||||||
|
padding: 10px;
|
||||||
|
color: $dark_gray;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #eee;
|
||||||
|
color: $dark_gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
font-weight: bold;
|
||||||
|
color: $black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-editor {
|
||||||
|
min-height: 500px;
|
||||||
|
|
||||||
|
p.description {
|
||||||
|
color: $dark_gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pagedown-editor {
|
||||||
|
width: 98%;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.plain {
|
||||||
|
width: 98%;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wmd-input {
|
||||||
|
width: 98%;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: 600px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.ace_editor {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
7
app/controllers/admin/site_content_types_controller.rb
Normal file
7
app/controllers/admin/site_content_types_controller.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class Admin::SiteContentTypesController < Admin::AdminController
|
||||||
|
|
||||||
|
def index
|
||||||
|
render_serialized(SiteContent.content_types, SiteContentTypeSerializer)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
15
app/controllers/admin/site_contents_controller.rb
Normal file
15
app/controllers/admin/site_contents_controller.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
class Admin::SiteContentsController < Admin::AdminController
|
||||||
|
|
||||||
|
def show
|
||||||
|
site_content = SiteContent.find_or_new(params[:id].to_s)
|
||||||
|
render_serialized(site_content, SiteContentSerializer)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
site_content = SiteContent.find_or_new(params[:id].to_s)
|
||||||
|
site_content.content = params[:content]
|
||||||
|
site_content.save!
|
||||||
|
|
||||||
|
render nothing: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -25,6 +25,7 @@ class Admin::UsersController < Admin::AdminController
|
||||||
@user.delete_all_posts!(guardian)
|
@user.delete_all_posts!(guardian)
|
||||||
render nothing: true
|
render nothing: true
|
||||||
end
|
end
|
||||||
|
|
||||||
def ban
|
def ban
|
||||||
@user = User.where(id: params[:user_id]).first
|
@user = User.where(id: params[:user_id]).first
|
||||||
guardian.ensure_can_ban!(@user)
|
guardian.ensure_can_ban!(@user)
|
||||||
|
|
24
app/models/site_content.rb
Normal file
24
app/models/site_content.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
require_dependency 'site_content_type'
|
||||||
|
require_dependency 'site_content_class_methods'
|
||||||
|
|
||||||
|
class SiteContent < ActiveRecord::Base
|
||||||
|
extend SiteContentClassMethods
|
||||||
|
|
||||||
|
set_primary_key :content_type
|
||||||
|
validates_presence_of :content
|
||||||
|
|
||||||
|
def self.formats
|
||||||
|
@formats ||= Enum.new(:plain, :markdown, :html, :css)
|
||||||
|
end
|
||||||
|
|
||||||
|
content_type :usage_tips, :markdown, default_18n_key: 'system_messages.usage_tips.text_body_template'
|
||||||
|
content_type :welcome_user, :markdown, default_18n_key: 'system_messages.welcome_user.text_body_template'
|
||||||
|
content_type :welcome_invite, :markdown, default_18n_key: 'system_messages.welcome_invite.text_body_template'
|
||||||
|
content_type :education_new_topic, :markdown, default_18n_key: 'education.new-topic'
|
||||||
|
content_type :education_new_reply, :markdown, default_18n_key: 'education.new-reply'
|
||||||
|
|
||||||
|
def site_content_type
|
||||||
|
@site_content_type ||= SiteContent.content_types.find {|t| t.content_type == content_type.to_sym}
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
28
app/models/site_content_type.rb
Normal file
28
app/models/site_content_type.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
require_dependency 'multisite_i18n'
|
||||||
|
|
||||||
|
class SiteContentType
|
||||||
|
|
||||||
|
attr_accessor :content_type, :format
|
||||||
|
|
||||||
|
def initialize(content_type, format, opts=nil)
|
||||||
|
@opts = opts || {}
|
||||||
|
@content_type = content_type
|
||||||
|
@format = format
|
||||||
|
end
|
||||||
|
|
||||||
|
def title
|
||||||
|
I18n.t("content_types.#{content_type}.title")
|
||||||
|
end
|
||||||
|
|
||||||
|
def description
|
||||||
|
I18n.t("content_types.#{content_type}.description")
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_content
|
||||||
|
if @opts[:default_18n_key].present?
|
||||||
|
return MultisiteI18n.t(@opts[:default_18n_key])
|
||||||
|
end
|
||||||
|
""
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
25
app/serializers/site_content_serializer.rb
Normal file
25
app/serializers/site_content_serializer.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
class SiteContentSerializer < ApplicationSerializer
|
||||||
|
|
||||||
|
attributes :content_type,
|
||||||
|
:title,
|
||||||
|
:description,
|
||||||
|
:content,
|
||||||
|
:format
|
||||||
|
|
||||||
|
def title
|
||||||
|
object.site_content_type.title
|
||||||
|
end
|
||||||
|
|
||||||
|
def description
|
||||||
|
object.site_content_type.description
|
||||||
|
end
|
||||||
|
|
||||||
|
def format
|
||||||
|
object.site_content_type.format
|
||||||
|
end
|
||||||
|
|
||||||
|
def content
|
||||||
|
return object.content if object.content.present?
|
||||||
|
object.site_content_type.default_content
|
||||||
|
end
|
||||||
|
end
|
13
app/serializers/site_content_type_serializer.rb
Normal file
13
app/serializers/site_content_type_serializer.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
class SiteContentTypeSerializer < ApplicationSerializer
|
||||||
|
|
||||||
|
attributes :content_type, :title
|
||||||
|
|
||||||
|
def content_type
|
||||||
|
object.content_type
|
||||||
|
end
|
||||||
|
|
||||||
|
def title
|
||||||
|
object.title
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -908,6 +908,11 @@ en:
|
||||||
approved_by: "approved by"
|
approved_by: "approved by"
|
||||||
time_read: "Read Time"
|
time_read: "Read Time"
|
||||||
|
|
||||||
|
site_content:
|
||||||
|
none: "Choose a type of content to begin editing."
|
||||||
|
title: 'Site Content'
|
||||||
|
edit: "Edit Site Content"
|
||||||
|
|
||||||
site_settings:
|
site_settings:
|
||||||
show_overriden: 'Only show overridden'
|
show_overriden: 'Only show overridden'
|
||||||
title: 'Site Settings'
|
title: 'Site Settings'
|
||||||
|
|
|
@ -310,6 +310,23 @@ en:
|
||||||
twitter_config_warning: 'The server is configured to allow signup and log in with Twitter (enable_twitter_logins), but the key and secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide#enable-twitter-logins" target="_blank">See this guide to learn more</a>.'
|
twitter_config_warning: 'The server is configured to allow signup and log in with Twitter (enable_twitter_logins), but the key and secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide#enable-twitter-logins" target="_blank">See this guide to learn more</a>.'
|
||||||
github_config_warning: 'The server is configured to allow signup and log in with GitHub (enable_github_logins), but the client id and secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide" target="_blank">See this guide to learn more</a>.'
|
github_config_warning: 'The server is configured to allow signup and log in with GitHub (enable_github_logins), but the client id and secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide" target="_blank">See this guide to learn more</a>.'
|
||||||
|
|
||||||
|
content_types:
|
||||||
|
education_new_reply:
|
||||||
|
title: "New User Education: New Reply"
|
||||||
|
description: "The pop up text a user sees when replying."
|
||||||
|
education_new_topic:
|
||||||
|
title: "New User Education: New Topic"
|
||||||
|
description: "The pop up text a user sees when creating a topic."
|
||||||
|
usage_tips:
|
||||||
|
title: "Usage Tips"
|
||||||
|
description: "The usage tips a new user receives when welcomed to the forum."
|
||||||
|
welcome_user:
|
||||||
|
title: "Welcome: New User"
|
||||||
|
description: "The system message a new user receives."
|
||||||
|
welcome_invite:
|
||||||
|
title: "Welcome: Invited User"
|
||||||
|
description: "The system message an invited user receives."
|
||||||
|
|
||||||
site_settings:
|
site_settings:
|
||||||
default_locale: "The default language of this Discourse instance (ISO 639-1 Code)"
|
default_locale: "The default language of this Discourse instance (ISO 639-1 Code)"
|
||||||
min_post_length: "Minimum post length in characters"
|
min_post_length: "Minimum post length in characters"
|
||||||
|
|
|
@ -54,6 +54,8 @@ Discourse::Application.routes.draw do
|
||||||
get 'flags/:filter' => 'flags#index'
|
get 'flags/:filter' => 'flags#index'
|
||||||
post 'flags/clear/:id' => 'flags#clear'
|
post 'flags/clear/:id' => 'flags#clear'
|
||||||
resources :site_customizations
|
resources :site_customizations
|
||||||
|
resources :site_contents
|
||||||
|
resources :site_content_types
|
||||||
resources :export
|
resources :export
|
||||||
get 'version_check' => 'versions#show'
|
get 'version_check' => 'versions#show'
|
||||||
resources :dashboard, only: [:index] do
|
resources :dashboard, only: [:index] do
|
||||||
|
|
0
db/fixtures/site_content_types.rb
Normal file
0
db/fixtures/site_content_types.rb
Normal file
10
db/migrate/20130404143437_create_site_contents.rb
Normal file
10
db/migrate/20130404143437_create_site_contents.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
class CreateSiteContents < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :site_contents, force: true, id: false do |t|
|
||||||
|
t.string :content_type, null: false
|
||||||
|
t.text :content, null: false
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
add_index :site_contents, :content_type, unique: true
|
||||||
|
end
|
||||||
|
end
|
32
lib/site_content_class_methods.rb
Normal file
32
lib/site_content_class_methods.rb
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
module SiteContentClassMethods
|
||||||
|
|
||||||
|
def content_types
|
||||||
|
@types || []
|
||||||
|
end
|
||||||
|
|
||||||
|
def content_type(content_type, format, opts=nil)
|
||||||
|
opts ||= {}
|
||||||
|
@types ||= []
|
||||||
|
@types << SiteContentType.new(content_type, format, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def content_for(content_type, replacements=nil)
|
||||||
|
replacements ||= {}
|
||||||
|
|
||||||
|
site_content = SiteContent.select(:content).where(content_type: content_type).first
|
||||||
|
return "" if site_content.blank?
|
||||||
|
|
||||||
|
site_content.content % replacements
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def find_or_new(content_type)
|
||||||
|
site_content = SiteContent.where(content_type: content_type).first
|
||||||
|
return site_content if site_content.present?
|
||||||
|
|
||||||
|
site_content = SiteContent.new
|
||||||
|
site_content.content_type = content_type
|
||||||
|
site_content
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
27
spec/controllers/admin/site_content_types_controller_spec.rb
Normal file
27
spec/controllers/admin/site_content_types_controller_spec.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Admin::SiteContentTypesController do
|
||||||
|
|
||||||
|
it "is a subclass of AdminController" do
|
||||||
|
(Admin::SiteContentTypesController < Admin::AdminController).should be_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
|
||||||
|
response.should be_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns JSON' do
|
||||||
|
xhr :get, :index
|
||||||
|
::JSON.parse(response.body).should be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
29
spec/controllers/admin/site_contents_controller_spec.rb
Normal file
29
spec/controllers/admin/site_contents_controller_spec.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Admin::SiteContentsController do
|
||||||
|
|
||||||
|
it "is a subclass of AdminController" do
|
||||||
|
(Admin::SiteContentsController < Admin::AdminController).should be_true
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'while logged in as an admin' do
|
||||||
|
before do
|
||||||
|
@user = log_in(:admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
context '.show' do
|
||||||
|
let(:content_type) { SiteContent.content_types.first.content_type }
|
||||||
|
|
||||||
|
it 'returns success' do
|
||||||
|
xhr :get, :show, id: content_type
|
||||||
|
response.should be_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns JSON' do
|
||||||
|
xhr :get, :show, id: content_type
|
||||||
|
::JSON.parse(response.body).should be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
9
spec/fabricators/site_content_fabricator.rb
Normal file
9
spec/fabricators/site_content_fabricator.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
Fabricator(:site_content) do
|
||||||
|
content_type 'great.poem'
|
||||||
|
content "%{flower} are red. %{food} are blue."
|
||||||
|
end
|
||||||
|
|
||||||
|
Fabricator(:site_content_basic, from: :site_content) do
|
||||||
|
content_type 'breaking.bad'
|
||||||
|
content "best show ever"
|
||||||
|
end
|
42
spec/models/site_content_spec.rb
Normal file
42
spec/models/site_content_spec.rb
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe SiteContent do
|
||||||
|
|
||||||
|
it { should validate_presence_of :content }
|
||||||
|
|
||||||
|
|
||||||
|
describe "#content_for" do
|
||||||
|
|
||||||
|
it "returns an empty string for a missing content_type" do
|
||||||
|
SiteContent.content_for('breaking.bad').should == ""
|
||||||
|
end
|
||||||
|
|
||||||
|
context "without replacements" do
|
||||||
|
let!(:site_content) { Fabricate(:site_content_basic) }
|
||||||
|
|
||||||
|
it "returns the simple string" do
|
||||||
|
SiteContent.content_for('breaking.bad').should == "best show ever"
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with replacements" do
|
||||||
|
let!(:site_content) { Fabricate(:site_content) }
|
||||||
|
let(:replacements) { {flower: 'roses', food: 'grapes'} }
|
||||||
|
|
||||||
|
it "returns the correct string with replacements" do
|
||||||
|
SiteContent.content_for('great.poem', replacements).should == "roses are red. grapes are blue."
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't mind extra keys in the replacements" do
|
||||||
|
SiteContent.content_for('great.poem', replacements.merge(extra: 'key')).should == "roses are red. grapes are blue."
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises an error with missing keys" do
|
||||||
|
-> { SiteContent.content_for('great.poem', flower: 'roses') }.should raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user