Version bump

This commit is contained in:
Neil Lalonde 2015-04-22 11:11:48 -04:00
commit bcccec1ea6
327 changed files with 6606 additions and 2147 deletions

View File

@ -33,6 +33,9 @@
"triggerEvent",
"count",
"exists",
"visible",
"invisible",
"selectDropdown",
"asyncTestDiscourse",
"fixture",
"find",

View File

@ -8,6 +8,9 @@ env:
- "RAILS_MASTER=1"
- "RAILS_MASTER=0"
addons:
postgresql: 9.3
matrix:
allow_failures:
- rvm: 2.0.0

11
Gemfile
View File

@ -244,8 +244,15 @@ gem 'simple-rss', require: false
# TODO mri_22 should be here, but bundler was real slow to pick it up
# not even in production bundler yet, monkey patching it in feels bad
gem 'gctools', require: false, platform: :mri_21
gem 'stackprof', require: false, platform: :mri_21
gem 'memory_profiler', require: false, platform: :mri_21
begin
gem 'stackprof', require: false, platform: [:mri_21, :mri_22]
gem 'memory_profiler', require: false, platform: [:mri_21, :mri_22]
rescue Bundler::GemfileError
STDERR.puts "You are running an old version of bundler, please upgrade bundler ASAP, if you are using Discourse docker, rebuild your container."
gem 'stackprof', require: false, platform: [:mri_21]
gem 'memory_profiler', require: false, platform: [:mri_21]
end
gem 'rmmseg-cpp', require: false

View File

@ -207,7 +207,7 @@ GEM
thor (~> 0.15)
libv8 (3.16.14.7)
listen (0.7.3)
logster (0.1.8)
logster (0.8.0)
lru_redux (0.8.4)
mail (2.5.4)
mime-types (~> 1.16)
@ -422,7 +422,7 @@ GEM
sprockets (~> 2.8)
stackprof (0.2.7)
stringex (2.5.2)
therubyracer (0.12.1)
therubyracer (0.12.2)
libv8 (~> 3.16.14.0)
ref
thin (1.6.3)

View File

@ -83,7 +83,7 @@ The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/A
## Copyright / License
Copyright 2014 Civilized Discourse Construction Kit, Inc.
Copyright 2014 - 2015 Civilized Discourse Construction Kit, Inc.
Licensed under the GNU General Public License Version 2.0 (or later);
you may not use this work except in compliance with the License.

View File

@ -1,6 +1,7 @@
import Presence from 'discourse/mixins/presence';
import { outputExportResult } from 'discourse/lib/export-result';
export default Ember.ArrayController.extend(Discourse.Presence, {
export default Ember.ArrayController.extend(Presence, {
loading: false,
actions: {

View File

@ -1,6 +1,7 @@
import Presence from 'discourse/mixins/presence';
import { outputExportResult } from 'discourse/lib/export-result';
export default Ember.ArrayController.extend(Discourse.Presence, {
export default Ember.ArrayController.extend(Presence, {
loading: false,
itemController: 'admin-log-screened-ip-address',
filter: null,

View File

@ -1,6 +1,7 @@
import Presence from 'discourse/mixins/presence';
import { outputExportResult } from 'discourse/lib/export-result';
export default Ember.ArrayController.extend(Discourse.Presence, {
export default Ember.ArrayController.extend(Presence, {
loading: false,
show() {

View File

@ -1,6 +1,7 @@
import Presence from 'discourse/mixins/presence';
import { outputExportResult } from 'discourse/lib/export-result';
export default Ember.ArrayController.extend(Discourse.Presence, {
export default Ember.ArrayController.extend(Presence, {
loading: false,
filters: null,

View File

@ -1,4 +1,6 @@
export default Ember.ArrayController.extend(Discourse.Presence, {
import Presence from 'discourse/mixins/presence';
export default Ember.ArrayController.extend(Presence, {
filter: null,
onlyOverridden: false,
filtered: Ember.computed.notEmpty('filter'),

View File

@ -1,6 +1,6 @@
const AdminUser = Discourse.User.extend({
customGroups: Em.computed.filter("groups", (g) => !g.automatic && g.visible && Discourse.Group.create(g)),
customGroups: Em.computed.filter("groups", (g) => !g.automatic && Discourse.Group.create(g)),
automaticGroups: Em.computed.filter("groups", (g) => g.automatic && Discourse.Group.create(g)),
generateApiKey() {

View File

@ -24,8 +24,8 @@ export default Ember.Route.extend({
},
editGroupings() {
const groupings = this.controllerFor('admin-badges').get('badgeGroupings');
showModal('modals/admin-edit-badge-groupings', groupings);
const model = this.controllerFor('admin-badges').get('badgeGroupings');
showModal('modals/admin-edit-badge-groupings', { model });
},
preview(badge, explain) {
@ -38,9 +38,9 @@ export default Ember.Route.extend({
trigger: badge.get('trigger'),
explain
}
}).then(function(json) {
}).then(function(model) {
badge.set('preview_loading', false);
showModal('modals/admin-badge-preview', json);
showModal('modals/admin-badge-preview', { model });
}).catch(function(error) {
badge.set('preview_loading', false);
Em.Logger.error(error);

View File

@ -12,13 +12,13 @@ export default Discourse.Route.extend({
},
actions: {
showAgreeFlagModal(flaggedPost) {
showModal('modals/admin-agree-flag', flaggedPost);
showAgreeFlagModal(model) {
showModal('modals/admin-agree-flag', { model });
this.controllerFor('modal').set('modalClass', 'agree-flag-modal');
},
showDeleteFlagModal(flaggedPost) {
showModal('modals/admin-delete-flag', flaggedPost);
showDeleteFlagModal(model) {
showModal('modals/admin-delete-flag', { model });
this.controllerFor('modal').set('modalClass', 'delete-flag-modal');
}

View File

@ -12,14 +12,14 @@ export default Discourse.Route.extend({
},
actions: {
showDetailsModal(logRecord) {
showModal('modals/admin-staff-action-log-details', logRecord);
showDetailsModal(model) {
showModal('modals/admin-staff-action-log-details', { model });
this.controllerFor('modal').set('modalClass', 'log-details-modal');
},
showCustomDetailsModal(logRecord) {
const modalName = "modals/" + (logRecord.action_name + '_details').replace("_", "-");
showModal(modalName, logRecord);
showCustomDetailsModal(model) {
const modalName = "modals/" + (model.action_name + '_details').replace("_", "-");
showModal(modalName, { model });
this.controllerFor('modal').set('modalClass', 'tabbed-modal log-details-modal');
}
}

View File

@ -24,8 +24,8 @@ export default Discourse.Route.extend({
},
actions: {
showSuspendModal(user) {
showModal('modals/admin-suspend-user', user);
showSuspendModal(model) {
showModal('modals/admin-suspend-user', { model });
this.controllerFor('modal').set('modalClass', 'suspend-user-modal');
}
}

View File

@ -1,4 +1,4 @@
<div class='span13'>
<div class='current-badge span13'>
<p>{{i18n 'admin.badges.none_selected'}}</p>
<div>

View File

@ -13,7 +13,9 @@
</ul>
</div>
<div class="pull-right">
{{d-button action="sendInvites" title="admin.invite.button_title" icon="user-plus" label="admin.invite.button_text"}}
{{#unless siteSettings.enable_sso}}
{{d-button action="sendInvites" title="admin.invite.button_title" icon="user-plus" label="admin.invite.button_text"}}
{{/unless}}
{{d-button action="exportUsers" title="admin.export_csv.button_title.user" icon="download" label="admin.export_csv.button_text"}}
</div>
</div>

View File

@ -0,0 +1,20 @@
import RestAdapter from 'discourse/adapters/rest';
import { Result } from 'discourse/adapters/rest';
export default RestAdapter.extend({
find(store, type, findArgs) {
return this._super(store, type, findArgs).then(function(result) {
return {post: result};
});
},
createRecord(store, type, args) {
const typeField = Ember.String.underscore(type);
args.nested_post = true;
return Discourse.ajax(this.pathFor(store, type), { method: 'POST', data: args }).then(function (json) {
return new Result(json[typeField], json);
});
}
});

View File

@ -1,5 +1,21 @@
const ADMIN_MODELS = ['plugin'];
export function Result(payload, responseJson) {
this.payload = payload;
this.responseJson = responseJson;
this.target = null;
}
const ajax = Discourse.ajax;
// We use this to make sure 404s are caught
function rethrow(error) {
if (error.status === 404) {
throw "404: " + error.responseText;
}
throw(error);
}
export default Ember.Object.extend({
pathFor(store, type, findArgs) {
let path = "/" + Ember.String.underscore(store.pluralize(type));
@ -25,21 +41,33 @@ export default Ember.Object.extend({
},
findAll(store, type) {
return Discourse.ajax(this.pathFor(store, type));
return ajax(this.pathFor(store, type)).catch(rethrow);
},
find(store, type, findArgs) {
return Discourse.ajax(this.pathFor(store, type, findArgs));
return ajax(this.pathFor(store, type, findArgs)).catch(rethrow);
},
update(store, type, id, attrs) {
const data = {};
data[Ember.String.underscore(type)] = attrs;
return Discourse.ajax(this.pathFor(store, type, id), { method: 'PUT', data });
return ajax(this.pathFor(store, type, id), { method: 'PUT', data }).then(function(json) {
return new Result(json[type], json);
});
},
createRecord(store, type, attrs) {
const data = {};
const typeField = Ember.String.underscore(type);
data[typeField] = attrs;
return ajax(this.pathFor(store, type), { method: 'POST', data }).then(function (json) {
return new Result(json[typeField], json);
});
},
destroyRecord(store, type, record) {
return Discourse.ajax(this.pathFor(store, type, record.get('id')), { method: 'DELETE' });
return ajax(this.pathFor(store, type, record.get('id')), { method: 'DELETE' });
}
});

View File

@ -0,0 +1,39 @@
import RestAdapter from 'discourse/adapters/rest';
function finderFor(filter, params) {
return function() {
let url = Discourse.getURL("/") + filter + ".json";
if (params) {
const keys = Object.keys(params),
encoded = [];
keys.forEach(function(p) {
const value = params[p];
if (typeof value !== 'undefined') {
encoded.push(p + "=" + value);
}
});
if (encoded.length > 0) {
url += "?" + encoded.join('&');
}
}
return Discourse.ajax(url);
};
}
export default RestAdapter.extend({
find(store, type, findArgs) {
const filter = findArgs.filter;
const params = findArgs.params;
return PreloadStore.getAndRemove("topic_list_" + filter, finderFor(filter, params)).then(function(result) {
result.filter = filter;
result.params = params;
return result;
});
}
});

View File

@ -3,7 +3,7 @@ import showModal from 'discourse/lib/show-modal';
export default Ember.Component.extend({
actions: {
showBulkActions() {
const controller = showModal('topicBulkActions', this.get('selected'));
const controller = showModal('topic-bulk-actions', { model: this.get('selected'), title: 'topics.bulk.actions' });
controller.set('refreshTarget', this.get('refreshTarget'));
}
}

View File

@ -10,20 +10,20 @@ export default Em.Component.extend(StringBuffer, {
rerenderTriggers: ['actionsHistory.@each', 'actionsHistory.users.length', 'post.deleted'],
// This was creating way too many bound ifs and subviews in the handlebars version.
renderString: function(buffer) {
renderString(buffer) {
if (!this.get('emptyHistory')) {
this.get('actionsHistory').forEach(function(c) {
buffer.push("<div class='post-action'>");
var renderActionIf = function(property, dataAttribute, text) {
const renderActionIf = function(property, dataAttribute, text) {
if (!c.get(property)) { return; }
buffer.push(" <span class='action-link " + dataAttribute +"-action'><a href='#' data-" + dataAttribute + "='" + c.get('id') + "'>" + text + "</a>.</span>");
};
// TODO multi line expansion for flags
var iconsHtml = "";
let iconsHtml = "";
if (c.get('usersExpanded')) {
var postUrl;
let postUrl;
c.get('users').forEach(function(u) {
iconsHtml += "<a href=\"" + Discourse.getURL("/users/") + u.get('username_lower') + "\" data-user-card=\"" + u.get('username_lower') + "\">";
if (u.post_url) {
@ -37,7 +37,7 @@ export default Em.Component.extend(StringBuffer, {
iconsHtml += "</a>";
});
var key = 'post.actions.people.' + c.get('actionType.name_key');
let key = 'post.actions.people.' + c.get('actionType.name_key');
if (postUrl) { key = key + "_with_url"; }
// TODO postUrl might be uninitialized? pick a good default
@ -52,7 +52,7 @@ export default Em.Component.extend(StringBuffer, {
});
}
var post = this.get('post');
const post = this.get('post');
if (post.get('deleted')) {
buffer.push("<div class='post-action'>" +
"<i class='fa fa-trash-o'></i>&nbsp;" +
@ -62,32 +62,34 @@ export default Em.Component.extend(StringBuffer, {
}
},
actionTypeById: function(actionTypeId) {
actionTypeById(actionTypeId) {
return this.get('actionsHistory').findProperty('id', actionTypeId);
},
click: function(e) {
var $target = $(e.target),
actionTypeId;
click(e) {
const $target = $(e.target);
let actionTypeId;
const post = this.get('post');
if (actionTypeId = $target.data('defer-flags')) {
this.actionTypeById(actionTypeId).deferFlags();
this.actionTypeById(actionTypeId).deferFlags(post);
return false;
}
// User wants to know who actioned it
if (actionTypeId = $target.data('who-acted')) {
this.actionTypeById(actionTypeId).loadUsers();
this.actionTypeById(actionTypeId).loadUsers(post);
return false;
}
if (actionTypeId = $target.data('act')) {
this.get('actionsHistory').findProperty('id', actionTypeId).act();
this.get('actionsHistory').findProperty('id', actionTypeId).act(post);
return false;
}
if (actionTypeId = $target.data('undo')) {
this.get('actionsHistory').findProperty('id', actionTypeId).undo();
this.get('actionsHistory').findProperty('id', actionTypeId).undo(post);
return false;
}

View File

@ -0,0 +1,6 @@
export default Ember.Component.extend({
tagName: 'a',
attributeBindings: ['href', 'data-user-card'],
href: Ember.computed.alias('user.path'),
'data-user-card': Ember.computed.alias('user.username_lower')
});

View File

@ -7,7 +7,8 @@ export default TextField.extend({
var self = this,
selected = [],
currentUser = this.currentUser,
includeGroups = this.get('includeGroups') === 'true';
includeGroups = this.get('includeGroups') === 'true',
allowedUsers = this.get('allowedUsers') === 'true';
function excludedUsernames() {
if (currentUser && self.get('excludeCurrentUser')) {
@ -27,7 +28,8 @@ export default TextField.extend({
term: term.replace(/[^a-zA-Z0-9_]/, ''),
topicId: self.get('topicId'),
exclude: excludedUsernames(),
includeGroups: includeGroups
includeGroups,
allowedUsers
});
},

View File

@ -61,7 +61,8 @@ export default DiscourseController.extend({
if (postId) {
this.set('model.loading', true);
const composer = this;
return Discourse.Post.load(postId).then(function(post) {
return this.store.find('post', postId).then(function(post) {
const quote = Discourse.Quote.build(post, post.get("raw"));
composer.appendBlockAtCursor(quote);
composer.set('model.loading', false);
@ -82,6 +83,13 @@ export default DiscourseController.extend({
},
hitEsc() {
const messages = this.get('controllers.composer-messages.model');
if (messages.length) {
messages.popObject();
return;
}
if (this.get('model.viewOpen')) {
this.shrink();
}
@ -217,13 +225,20 @@ export default DiscourseController.extend({
const promise = composer.save({
imageSizes: this.get('view').imageSizes(),
editReason: this.get("editReason")
}).then(function(opts) {
}).then(function(result) {
if (result.responseJson.action === "enqueued") {
self.send('postWasEnqueued');
self.destroyDraft();
self.close();
return result;
}
// If we replied as a new topic successfully, remove the draft.
if (self.get('replyAsNewTopicDraft')) {
self.destroyDraft();
}
opts = opts || {};
self.close();
const currentUser = Discourse.User.current();
@ -236,16 +251,16 @@ export default DiscourseController.extend({
// TODO disableJumpReply is super crude, it needs to provide some sort
// of notification to the end user
if (!composer.get('replyingToTopic') || !disableJumpReply) {
if (opts.post && !staged) {
Discourse.URL.routeTo(opts.post.get('url'));
const post = result.target;
if (post && !staged) {
Discourse.URL.routeTo(post.get('url'));
}
}
}).catch(function(error) {
composer.set('disableDrafts', false);
bootbox.alert(error);
self.appEvents.one('composer:opened', () => bootbox.alert(error));
});
if (this.get('controllers.application.currentRouteName').split('.')[0] === 'topic' &&
composer.get('topic.id') === this.get('controllers.topic.model.id')) {
staged = composer.get('stagedPost');
@ -412,7 +427,7 @@ export default DiscourseController.extend({
composerModel.set('topic', opts.topic);
}
} else {
composerModel = composerModel || Discourse.Composer.create();
composerModel = composerModel || this.store.createRecord('composer');
composerModel.open(opts);
}

View File

@ -1 +1,3 @@
export default Ember.Controller.extend(Discourse.Presence);
import Presence from 'discourse/mixins/presence';
export default Ember.Controller.extend(Presence);

View File

@ -48,7 +48,8 @@ var controllerOpts = {
// router and ember throws an error due to missing `handlerInfos`.
// Lesson learned: Don't call `loading` yourself.
this.set('controllers.discovery.loading', true);
Discourse.TopicList.find(filter).then(function(list) {
this.store.findFiltered('topicList', {filter}).then(function(list) {
Discourse.TopicList.hideUniformCategory(list, self.get('category'));
self.setProperties({ model: list });

View File

@ -81,7 +81,8 @@ export default ObjectController.extend(ModalFunctionality, {
if (opts) params = $.extend(params, opts);
this.send('hideModal');
postAction.act(params).then(function() {
postAction.act(this.get('model'), params).then(function() {
self.send('closeModal');
}, function(errors) {
self.send('closeModal');

View File

@ -99,6 +99,7 @@ HeaderController.reopenClass({
});
addFlagProperty('currentUser.site_flagged_posts_count');
addFlagProperty('currentUser.post_queue_new_count');
export { addFlagProperty };
export default HeaderController;

View File

@ -15,11 +15,15 @@ export default ObjectController.extend(ModalFunctionality, {
disabled: function() {
if (this.get('saving')) return true;
if (this.blank('emailOrUsername')) return true;
// when inviting to forum, email must be valid
if (!this.get('invitingToTopic') && !Discourse.Utilities.emailValid(this.get('emailOrUsername'))) return true;
// normal users (not admin) can't invite users to private topic via email
if (!this.get('isAdmin') && this.get('isPrivateTopic') && Discourse.Utilities.emailValid(this.get('emailOrUsername'))) return true;
// when invting to private topic via email, group name must be specified
if (this.get('isPrivateTopic') && this.blank('groupNames') && Discourse.Utilities.emailValid(this.get('emailOrUsername'))) return true;
if (this.get('model.details.can_invite_to')) return false;
if (this.get('isPrivateTopic') && this.blank('groupNames')) return true;
return false;
}.property('emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'groupNames', 'saving'),
}.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'groupNames', 'saving'),
buttonTitle: function() {
return this.get('saving') ? I18n.t('topic.inviting') : I18n.t('topic.invite_reply.action');
@ -31,20 +35,23 @@ export default ObjectController.extend(ModalFunctionality, {
return this.get('model') !== Discourse.User.current();
}.property('model'),
topicId: Ember.computed.alias('model.id'),
// Is Private Topic? (i.e. visible only to specific group members)
isPrivateTopic: Em.computed.and('invitingToTopic', 'model.category.read_restricted'),
// Is Private Message?
isMessage: Em.computed.equal('model.archetype', 'private_message'),
// Allow Existing Members? (username autocomplete)
allowExistingMembers: function() {
return this.get('invitingToTopic') && !this.get('isPrivateTopic');
}.property('invitingToTopic', 'isPrivateTopic'),
return this.get('invitingToTopic');
}.property('invitingToTopic'),
// Show Groups? (add invited user to private group)
showGroups: function() {
return this.get('isAdmin') && (Discourse.Utilities.emailValid(this.get('emailOrUsername')) || this.get('isPrivateTopic') || !this.get('invitingToTopic')) && !Discourse.SiteSettings.enable_sso;
}.property('isAdmin', 'emailOrUsername', 'isPrivateTopic', 'invitingToTopic'),
return this.get('isAdmin') && (Discourse.Utilities.emailValid(this.get('emailOrUsername')) || this.get('isPrivateTopic') || !this.get('invitingToTopic')) && !Discourse.SiteSettings.enable_sso && !this.get('isMessage');
}.property('isAdmin', 'emailOrUsername', 'isPrivateTopic', 'isMessage', 'invitingToTopic'),
// Instructional text for the modal.
inviteInstructions: function() {
@ -55,13 +62,19 @@ export default ObjectController.extend(ModalFunctionality, {
// inviting to a message
return I18n.t('topic.invite_private.email_or_username');
} else if (this.get('invitingToTopic')) {
// when inviting to topic, display instructions based on provided entity
if (this.blank('emailOrUsername')) {
return I18n.t('topic.invite_reply.to_topic_blank');
} else if (Discourse.Utilities.emailValid(this.get('emailOrUsername'))) {
return I18n.t('topic.invite_reply.to_topic_email');
// inviting to a private/public topic
if (this.get('isPrivateTopic') && !this.get('isAdmin')) {
// inviting to a private topic and is not admin
return I18n.t('topic.invite_reply.to_username');
} else {
return I18n.t('topic.invite_reply.to_topic_username');
// when inviting to a topic, display instructions based on provided entity
if (this.blank('emailOrUsername')) {
return I18n.t('topic.invite_reply.to_topic_blank');
} else if (Discourse.Utilities.emailValid(this.get('emailOrUsername'))) {
return I18n.t('topic.invite_reply.to_topic_email');
} else {
return I18n.t('topic.invite_reply.to_topic_username');
}
}
} else {
// inviting to forum

View File

@ -20,7 +20,7 @@ export default ObjectController.extend({
var badgeId = this.safe("data.badge_id");
if (badgeId) {
var badgeName = this.safe("data.badge_name");
return '/badges/' + badgeId + '/' + badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase();
return Discourse.getURL('/badges/' + badgeId + '/' + badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase());
}
var topicId = this.safe('topic_id');
@ -29,7 +29,7 @@ export default ObjectController.extend({
}
if (this.get('notification_type') === INVITED_TYPE) {
return '/my/invited';
return Discourse.getURL('/my/invited');
}
}.property("data.{badge_id,badge_name}", "slug", "topic_id", "post_number"),

View File

@ -1,4 +1,6 @@
export default Ember.ArrayController.extend({
needs: ['header'],
loadingNotifications: Em.computed.alias('controllers.header.loadingNotifications')
loadingNotifications: Em.computed.alias('controllers.header.loadingNotifications'),
myNotificationsUrl: Discourse.computed.url('/my/notifications')
});

View File

@ -1 +1,3 @@
export default Ember.ObjectController.extend(Discourse.Presence);
import Presence from 'discourse/mixins/presence';
export default Ember.ObjectController.extend(Presence);

View File

@ -67,7 +67,7 @@ export default ObjectController.extend(CanCheckEmails, {
{ name: I18n.t('user.email_digests.every_two_weeks'), value: 14 }],
autoTrackDurations: [{ name: I18n.t('user.auto_track_options.never'), value: -1 },
{ name: I18n.t('user.auto_track_options.always'), value: 0 },
{ name: I18n.t('user.auto_track_options.immediately'), value: 0 },
{ name: I18n.t('user.auto_track_options.after_n_seconds', { count: 30 }), value: 30000 },
{ name: I18n.t('user.auto_track_options.after_n_minutes', { count: 1 }), value: 60000 },
{ name: I18n.t('user.auto_track_options.after_n_minutes', { count: 2 }), value: 120000 },

View File

@ -0,0 +1,42 @@
import BufferedContent from 'discourse/mixins/buffered-content';
import { popupAjaxError } from 'discourse/lib/ajax-error';
function updateState(state) {
return function() {
const post = this.get('post');
post.update({ state }).then(() => {
this.get('controllers.queued-posts.model').removeObject(post);
}).catch(popupAjaxError);
};
}
export default Ember.Controller.extend(BufferedContent, {
needs: ['queued-posts'],
post: Ember.computed.alias('model'),
currentlyEditing: Ember.computed.alias('controllers.queued-posts.editing'),
editing: Discourse.computed.propertyEqual('model', 'currentlyEditing'),
actions: {
approve: updateState('approved'),
reject: updateState('rejected'),
edit() {
// This is stupid but pagedown cannot be on the screen twice or it will break
this.set('currentlyEditing', null);
Ember.run.scheduleOnce('afterRender', () => this.set('currentlyEditing', this.get('model')));
},
confirmEdit() {
this.get('post').update({ raw: this.get('buffered.raw') }).then(() => {
this.commitBuffer();
this.set('currentlyEditing', null);
});
},
cancelEdit() {
this.rollbackBuffer();
this.set('currentlyEditing', null);
}
}
});

View File

@ -1,8 +1,9 @@
import Presence from 'discourse/mixins/presence';
import searchForTerm from 'discourse/lib/search-for-term';
var _dontSearch = false;
export default Em.Controller.extend(Discourse.Presence, {
export default Em.Controller.extend(Presence, {
contextType: function(key, value){
if(arguments.length > 1) {

View File

@ -8,7 +8,7 @@ export default Ember.ArrayController.extend({
return Discourse.SiteSettings.faq_url ? Discourse.SiteSettings.faq_url : Discourse.getURL('/faq');
}.property(),
badgesUrl: Discourse.getURL('/badges'),
badgesUrl: Discourse.computed.url('/badges'),
showKeyboardShortcuts: function(){
return !Discourse.Mobile.mobileView && !this.capabilities.touch;

View File

@ -150,7 +150,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon
toggleLike(post) {
const likeAction = post.get('actionByName.like');
if (likeAction && likeAction.get('canToggle')) {
likeAction.toggle();
likeAction.toggle(post);
}
},

View File

@ -19,10 +19,7 @@ export default ObjectController.extend(CanCheckEmails, {
linkWebsite: Em.computed.not('isBasic'),
canSeePrivateMessages: function() {
return this.get('viewingSelf') || Discourse.User.currentProp('admin');
}.property('viewingSelf'),
canSeePrivateMessages: Ember.computed.or('viewingSelf', 'currentUser.admin'),
canSeeNotificationHistory: Em.computed.alias('canSeePrivateMessages'),
showBadges: function() {

View File

@ -0,0 +1,6 @@
import registerUnbound from 'discourse/helpers/register-unbound';
registerUnbound('cook-text', function(text) {
return new Handlebars.SafeString(Discourse.Markdown.cook(text));
});

View File

@ -27,10 +27,13 @@ export default {
bus.callbackInterval = siteSettings.polling_interval;
bus.enableLongPolling = true;
if (user.admin || user.moderator) {
bus.subscribe('/flagged_counts', function(data) {
if (user.get('staff')) {
bus.subscribe('/flagged_counts', (data) => {
user.set('site_flagged_posts_count', data.total);
});
bus.subscribe('/queue_counts', (data) => {
user.set('post_queue_new_count', data.post_queue_new_count);
});
}
bus.subscribe("/notification/" + user.get('id'), (function(data) {
const oldUnread = user.get('unread_notifications');

View File

@ -5,5 +5,6 @@ export default {
// URL rewrites (usually due to refactoring)
Discourse.URL.rewrite(/^\/category\//, "/c/");
Discourse.URL.rewrite(/^\/group\//, "/groups/");
Discourse.URL.rewrite(/\/private-messages\/$/, "/messages/");
}
};

View File

@ -323,7 +323,13 @@
// Adds a listener callback to a DOM element which is fired on a specified
// event.
util.addEvent = function (elem, event, listener) {
elem.addEventListener(event, listener, false);
var wrapped = function() {
var wrappedArgs = Array.prototype.slice.call(arguments);
Ember.run(function() {
listener.apply(this, wrappedArgs);
});
};
elem.addEventListener(event, wrapped, false);
};
@ -904,7 +910,7 @@
// TODO allow us to inject this in (its our debouncer)
var debounce = function(func,wait,trickle) {
var timeout = null;
return function(){
return function() {
var context = this;
var args = arguments;
@ -924,8 +930,8 @@
currentWait = wait;
}
if (timeout) { clearTimeout(timeout); }
timeout = setTimeout(later, currentWait);
if (timeout) { Ember.run.cancel(timeout); }
timeout = Ember.run.later(later, currentWait);
}
}

View File

@ -0,0 +1,38 @@
function extractError(error) {
if (error instanceof Error) {
Ember.Logger.error(error.stack);
}
if (typeof error === "string") {
Ember.Logger.error(error);
}
let parsedError;
if (error.responseText) {
try {
const parsedJSON = $.parseJSON(error.responseText);
if (parsedJSON.errors) {
parsedError = parsedJSON.errors[0];
} else if (parsedJSON.failed) {
parsedError = parsedJSON.message;
}
} catch(ex) {
// in case the JSON doesn't parse
Ember.Logger.error(ex.stack);
}
}
return parsedError || I18n.t('generic_error');
}
export function throwAjaxError(undoCallback) {
return function(error) {
// If we provided an `undo` callback
if (undoCallback) { undoCallback(error); }
throw extractError(error);
};
}
export function popupAjaxError(err) {
bootbox.alert(extractError(err));
}

View File

@ -51,7 +51,8 @@ Discourse.Onebox = {
// Retrieve the onebox
var promise = Discourse.ajax("/onebox", {
dataType: 'html',
data: { url: url, refresh: refresh }
data: { url: url, refresh: refresh },
cache: true
});
// We can call this when loading is complete

View File

@ -1,13 +1,38 @@
export default (name, model) => {
export default (name, opts) => {
opts = opts || {};
if (opts.__type) {
Ember.warn("showModal now takes `opts` as a second param instead of a model");
opts = {model: opts};
}
const container = Discourse.__container__;
// We use the container here because modals are like singletons
// in Discourse. Only one can be shown with a particular state.
const route = Discourse.__container__.lookup('route:application');
const route = container.lookup('route:application');
const modalController = route.controllerFor('modal');
route.controllerFor('modal').set('modalClass', null);
route.render(name, { into: 'modal', outlet: 'modalBody' });
modalController.set('modalClass', null);
const viewClass = container.lookupFactory('view:' + name);
const controller = container.lookup('controller:' + name);
if (viewClass) {
route.render(name, { into: 'modal', outlet: 'modalBody' });
} else {
const templateName = Ember.String.dasherize(name);
const renderArgs = { into: 'modal', outlet: 'modalBody', view: 'modal-body'};
if (controller) { renderArgs.controller = name; }
route.render('modal/' + templateName, renderArgs);
if (opts.title) {
modalController.set('title', I18n.t(opts.title));
}
}
const controller = route.controllerFor(name);
if (controller) {
const model = opts.model;
if (model) { controller.set('model', model); }
if (controller.onShow) { controller.onShow(); }
controller.set('flashMessage', null);

View File

@ -6,7 +6,7 @@ var cache = {},
currentTerm,
oldSearch;
function performSearch(term, topicId, includeGroups, resultsFn) {
function performSearch(term, topicId, includeGroups, allowedUsers, resultsFn) {
var cached = cache[term];
if (cached) {
resultsFn(cached);
@ -17,7 +17,8 @@ function performSearch(term, topicId, includeGroups, resultsFn) {
oldSearch = $.ajax(Discourse.getURL('/users/search/users'), {
data: { term: term,
topic_id: topicId,
include_groups: includeGroups }
include_groups: includeGroups,
topic_allowed_users: allowedUsers }
});
var returnVal = CANCELLED_STATUS;
@ -75,6 +76,7 @@ function organizeResults(r, options) {
export default function userSearch(options) {
var term = options.term || "",
includeGroups = options.includeGroups,
allowedUsers = options.allowedUsers,
topicId = options.topicId;
@ -101,7 +103,7 @@ export default function userSearch(options) {
resolve(CANCELLED_STATUS);
}, 5000);
debouncedSearch(term, topicId, includeGroups, function(r) {
debouncedSearch(term, topicId, includeGroups, allowedUsers, function(r) {
clearTimeout(clearPromise);
resolve(organizeResults(r, options));
});

View File

@ -51,7 +51,7 @@ Discourse.Utilities = {
var classes = "avatar" + (options.extraClasses ? " " + options.extraClasses : "");
var title = (options.title) ? " title='" + Handlebars.Utils.escapeExpression(options.title || "") + "'" : "";
return "<img width='" + size + "' height='" + size + "' src='" + url + "' class='" + classes + "'" + title + ">";
return "<img alt='' width='" + size + "' height='" + size + "' src='" + url + "' class='" + classes + "'" + title + ">";
},
tinyAvatar: function(avatarTemplate, options) {

View File

@ -1,36 +0,0 @@
/**
This mixin provides `blank` and `present` to determine whether properties are
there, accounting for more cases than just null and undefined.
@class Discourse.Presence
@extends Ember.Mixin
@namespace Discourse
@module Discourse
**/
Discourse.Presence = Em.Mixin.create({
/**
Returns whether a property is blank. It considers empty arrays, string, objects, undefined and null
to be blank, otherwise true.
@method blank
@param {String} name the name of the property we want to check
@return {Boolean}
*/
blank: function(name) {
return Ember.isEmpty(this[name] || this.get(name));
},
/**
Returns whether a property is present. A present property is the opposite of a `blank` one.
@method present
@param {String} name the name of the property we want to check
@return {Boolean}
*/
present: function(name) {
return !this.blank(name);
}
});

View File

@ -0,0 +1,20 @@
/**
This mixin provides `blank` and `present` to determine whether properties are
there, accounting for more cases than just null and undefined.
**/
export default Ember.Mixin.create({
/**
Returns whether a property is blank. It considers empty arrays, string, objects, undefined and null
to be blank, otherwise true.
*/
blank(name) {
return Ember.isEmpty(this[name] || this.get(name));
},
// Returns whether a property is present. A present property is the opposite of a `blank` one.
present(name) {
return !this.blank(name);
}
});

View File

@ -1,11 +1,3 @@
/**
A data model for summarizing actions a user has taken, for example liking a post.
@class ActionSummary
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.ActionSummary = Discourse.Model.extend({
// Description for the action
@ -44,16 +36,16 @@ Discourse.ActionSummary = Discourse.Model.extend({
}
},
toggle: function() {
toggle: function(post) {
if (!this.get('acted')) {
this.act();
this.act(post);
} else {
this.undo();
this.undo(post);
}
},
// Perform this action
act: function(opts) {
act: function(post, opts) {
if (!opts) opts = {};
var action = this.get('actionType.name_key');
@ -82,18 +74,14 @@ Discourse.ActionSummary = Discourse.Model.extend({
return Discourse.ajax("/post_actions", {
type: 'POST',
data: {
id: this.get('flagTopic') ? this.get('flagTopic.id') : this.get('post.id'),
id: this.get('flagTopic') ? this.get('flagTopic.id') : post.get('id'),
post_action_type_id: this.get('id'),
message: opts.message,
take_action: opts.takeAction,
flag_topic: this.get('flagTopic') ? true : false
}
}).then(function(result) {
var post = self.get('post');
if (post && result && result.id === post.get('id')) {
post.updateFromJson(result);
}
return post;
return post.updateActionsSummary(result);
}).catch(function (error) {
self.removeAction();
var message = $.parseJSON(error.responseText).errors;
@ -102,43 +90,38 @@ Discourse.ActionSummary = Discourse.Model.extend({
},
// Undo this action
undo: function() {
undo: function(post) {
this.removeAction();
// Remove our post action
var self = this;
return Discourse.ajax("/post_actions/" + (this.get('post.id')), {
return Discourse.ajax("/post_actions/" + post.get('id'), {
type: 'DELETE',
data: {
post_action_type_id: this.get('id')
}
}).then(function(result) {
var post = self.get('post');
if (post && result && result.id === post.get('id')) {
post.updateFromJson(result);
}
return post;
return post.updateActionsSummary(result);
});
},
deferFlags: function() {
deferFlags: function(post) {
var self = this;
return Discourse.ajax("/post_actions/defer_flags", {
type: "POST",
data: {
post_action_type_id: this.get("id"),
id: this.get("post.id")
id: post.get('id')
}
}).then(function () {
self.set("count", 0);
});
},
loadUsers: function() {
loadUsers: function(post) {
var self = this;
Discourse.ajax("/post_actions/users", {
data: {
id: this.get('post.id'),
id: post.get('id'),
post_action_type_id: this.get('id')
}
}).then(function (result) {

View File

@ -1,3 +1,7 @@
import RestModel from 'discourse/models/rest';
import Topic from 'discourse/models/topic';
import { throwAjaxError } from 'discourse/lib/ajax-error';
const CLOSED = 'closed',
SAVING = 'saving',
OPEN = 'open',
@ -26,10 +30,10 @@ const CLOSED = 'closed',
categoryId: 'topic.category.id'
};
const Composer = Discourse.Model.extend({
const Composer = RestModel.extend({
archetypes: function() {
return Discourse.Site.currentProp('archetypes');
return this.site.get('archetypes');
}.property(),
creatingTopic: Em.computed.equal('action', CREATE_TOPIC),
@ -127,21 +131,16 @@ const Composer = Discourse.Model.extend({
} else {
// has a category? (when needed)
return this.get('canCategorize') &&
!Discourse.SiteSettings.allow_uncategorized_topics &&
!this.siteSettings.allow_uncategorized_topics &&
!this.get('categoryId') &&
!Discourse.User.currentProp('staff');
!this.user.get('staff');
}
}.property('loading', 'canEditTitle', 'titleLength', 'targetUsernames', 'replyLength', 'categoryId', 'missingReplyCharacters'),
/**
Is the title's length valid?
@property titleLengthValid
**/
titleLengthValid: function() {
if (Discourse.User.currentProp('admin') && this.get('post.static_doc') && this.get('titleLength') > 0) return true;
if (this.user.get('admin') && this.get('post.static_doc') && this.get('titleLength') > 0) return true;
if (this.get('titleLength') < this.get('minimumTitleLength')) return false;
return (this.get('titleLength') <= Discourse.SiteSettings.max_topic_title_length);
return (this.get('titleLength') <= this.siteSettings.max_topic_title_length);
}.property('minimumTitleLength', 'titleLength', 'post.static_doc'),
// The icon for the save button
@ -194,9 +193,9 @@ const Composer = Discourse.Model.extend({
**/
minimumTitleLength: function() {
if (this.get('privateMessage')) {
return Discourse.SiteSettings.min_private_message_title_length;
return this.siteSettings.min_private_message_title_length;
} else {
return Discourse.SiteSettings.min_topic_title_length;
return this.siteSettings.min_topic_title_length;
}
}.property('privateMessage'),
@ -216,12 +215,12 @@ const Composer = Discourse.Model.extend({
**/
minimumPostLength: function() {
if( this.get('privateMessage') ) {
return Discourse.SiteSettings.min_private_message_post_length;
return this.siteSettings.min_private_message_post_length;
} else if (this.get('topicFirstPost')) {
// first post (topic body)
return Discourse.SiteSettings.min_first_post_length;
return this.siteSettings.min_first_post_length;
} else {
return Discourse.SiteSettings.min_post_length;
return this.siteSettings.min_post_length;
}
}.property('privateMessage', 'topicFirstPost'),
@ -249,7 +248,7 @@ const Composer = Discourse.Model.extend({
_setupComposer: function() {
const val = (Discourse.Mobile.mobileView ? false : (Discourse.KeyValueStore.get('composer.showPreview') || 'true'));
this.set('showPreview', val === 'true');
this.set('archetypeId', Discourse.Site.currentProp('default_archetype'));
this.set('archetypeId', this.site.get('default_archetype'));
}.on('init'),
/**
@ -349,15 +348,15 @@ const Composer = Discourse.Model.extend({
this.setProperties({
categoryId: opts.categoryId || this.get('topic.category.id'),
archetypeId: opts.archetypeId || Discourse.Site.currentProp('default_archetype'),
archetypeId: opts.archetypeId || this.site.get('default_archetype'),
metaData: opts.metaData ? Em.Object.create(opts.metaData) : null,
reply: opts.reply || this.get("reply") || ""
});
if (opts.postId) {
this.set('loading', true);
Discourse.Post.load(opts.postId).then(function(result) {
composer.set('post', result);
this.store.find('post', opts.postId).then(function(post) {
composer.set('post', post);
composer.set('loading', false);
});
}
@ -370,10 +369,10 @@ const Composer = Discourse.Model.extend({
this.setProperties(topicProps);
Discourse.Post.load(opts.post.get('id')).then(function(result) {
this.store.find('post', opts.post.get('id')).then(function(post) {
composer.setProperties({
reply: result.get('raw'),
originalText: result.get('raw'),
reply: post.get('raw'),
originalText: post.get('raw'),
loading: false
});
});
@ -425,30 +424,29 @@ const Composer = Discourse.Model.extend({
post.get('post_number') === 1 &&
this.get('topic.details.can_edit')) {
const topicProps = this.getProperties(Object.keys(_edit_topic_serializer));
promise = Discourse.Topic.update(this.get('topic'), topicProps);
promise = Topic.update(this.get('topic'), topicProps);
} else {
promise = Ember.RSVP.resolve();
}
post.setProperties({
const props = {
raw: this.get('reply'),
editReason: opts.editReason,
imageSizes: opts.imageSizes,
edit_reason: opts.editReason,
image_sizes: opts.imageSizes,
cooked: this.getCookedHtml()
});
};
this.set('composeState', CLOSED);
return promise.then(function() {
return post.save(function(result) {
post.updateFromPost(result);
return post.save(props).then(function(result) {
self.clearState();
}, function (error) {
return result;
}).catch(throwAjaxError(function() {
post.set('cooked', oldCooked);
self.set('composeState', OPEN);
const response = $.parseJSON(error.responseText);
throw response && response.errors ? response.errors[0] : I18n.t('generic_error');
});
}));
});
},
@ -467,30 +465,30 @@ const Composer = Discourse.Model.extend({
createPost(opts) {
const post = this.get('post'),
topic = this.get('topic'),
currentUser = Discourse.User.current(),
user = this.user,
postStream = this.get('topic.postStream');
let addedToStream = false;
// Build the post object
const createdPost = Discourse.Post.create({
const createdPost = this.store.createRecord('post', {
imageSizes: opts.imageSizes,
cooked: this.getCookedHtml(),
reply_count: 0,
name: currentUser.get('name'),
display_username: currentUser.get('name'),
username: currentUser.get('username'),
user_id: currentUser.get('id'),
user_title: currentUser.get('title'),
uploaded_avatar_id: currentUser.get('uploaded_avatar_id'),
user_custom_fields: currentUser.get('custom_fields'),
post_type: Discourse.Site.currentProp('post_types.regular'),
name: user.get('name'),
display_username: user.get('name'),
username: user.get('username'),
user_id: user.get('id'),
user_title: user.get('title'),
uploaded_avatar_id: user.get('uploaded_avatar_id'),
user_custom_fields: user.get('custom_fields'),
post_type: this.site.get('post_types.regular'),
actions_summary: [],
moderator: currentUser.get('moderator'),
admin: currentUser.get('admin'),
moderator: user.get('moderator'),
admin: user.get('admin'),
yours: true,
newPost: true,
read: true
read: true,
wiki: false
});
this.serialize(_create_serializer, createdPost);
@ -520,78 +518,55 @@ const Composer = Discourse.Model.extend({
// we would need to handle oneboxes and other bits that are not even in the
// engine, staging will just cause a blank post to render
if (!_.isEmpty(createdPost.get('cooked'))) {
state = postStream.stagePost(createdPost, currentUser);
if(state === "alreadyStaging"){
return;
}
state = postStream.stagePost(createdPost, user);
if (state === "alreadyStaging") { return; }
}
}
const composer = this,
promise = new Ember.RSVP.Promise(function(resolve, reject) {
composer.set('composeState', SAVING);
createdPost.save(function(result) {
let saving = true;
createdPost.updateFromJson(result);
if (topic) {
// It's no longer a new post
createdPost.set('newPost', false);
topic.set('draft_sequence', result.draft_sequence);
postStream.commitPost(createdPost);
addedToStream = true;
} else {
// We created a new topic, let's show it.
composer.set('composeState', CLOSED);
saving = false;
// Update topic_count for the category
const category = Discourse.Site.currentProp('categories').find(function(x) { return x.get('id') === (parseInt(createdPost.get('category'),10) || 1); });
if (category) category.incrementProperty('topic_count');
Discourse.notifyPropertyChange('globalNotice');
}
composer.clearState();
composer.set('createdPost', createdPost);
if (addedToStream) {
composer.set('composeState', CLOSED);
} else if (saving) {
composer.set('composeState', SAVING);
}
return resolve({ post: result });
}, function(error) {
// If an error occurs
if (postStream) {
postStream.undoPost(createdPost);
}
composer.set('composeState', OPEN);
// TODO extract error handling code
let parsedError;
try {
const parsedJSON = $.parseJSON(error.responseText);
if (parsedJSON.errors) {
parsedError = parsedJSON.errors[0];
} else if (parsedJSON.failed) {
parsedError = parsedJSON.message;
}
}
catch(ex) {
parsedError = "Unknown error saving post, try again. Error: " + error.status + " " + error.statusText;
}
reject(parsedError);
});
});
const composer = this;
composer.set('composeState', SAVING);
composer.set("stagedPost", state === "staged" && createdPost);
return promise;
return createdPost.save().then(function(result) {
let saving = true;
if (result.responseJson.action === "enqueued") {
if (postStream) { postStream.undoPost(createdPost); }
return result;
}
if (topic) {
// It's no longer a new post
topic.set('draft_sequence', result.target.draft_sequence);
postStream.commitPost(createdPost);
addedToStream = true;
} else {
// We created a new topic, let's show it.
composer.set('composeState', CLOSED);
saving = false;
// Update topic_count for the category
const category = composer.site.get('categories').find(function(x) { return x.get('id') === (parseInt(createdPost.get('category'),10) || 1); });
if (category) category.incrementProperty('topic_count');
Discourse.notifyPropertyChange('globalNotice');
}
composer.clearState();
composer.set('createdPost', createdPost);
if (addedToStream) {
composer.set('composeState', CLOSED);
} else if (saving) {
composer.set('composeState', SAVING);
}
return result;
}).catch(throwAjaxError(function() {
if (postStream) {
postStream.undoPost(createdPost);
}
Ember.run.next(() => composer.set('composeState', OPEN));
}));
},
getCookedHtml() {
@ -604,7 +579,7 @@ const Composer = Discourse.Model.extend({
// Do not save when there is no reply
if (!this.get('reply')) return;
// Do not save when the reply's length is too small
if (this.get('replyLength') < Discourse.SiteSettings.min_post_length) return;
if (this.get('replyLength') < this.siteSettings.min_post_length) return;
const data = {
reply: this.get('reply'),
@ -673,6 +648,14 @@ Composer.reopenClass({
}
},
create(args) {
args = args || {};
args.user = args.user || Discourse.User.current();
args.site = args.site || Discourse.Site.current();
args.siteSettings = args.siteSettings || Discourse.SiteSettings;
return this._super(args);
},
serializeToTopic(fieldName, property) {
if (!property) { property = fieldName; }
_edit_topic_serializer[fieldName] = property;

View File

@ -52,6 +52,12 @@ const Group = Discourse.Model.extend({
}).then(function() {
// reload member list
self.findMembers();
}).catch(function(error) {
if (error && error.responseText) {
bootbox.alert($.parseJSON(error.responseText).errors[0]);
} else {
bootbox.alert(I18n.t('generic_error'));
}
});
},

View File

@ -1,6 +1,8 @@
Discourse.Model = Ember.Object.extend(Discourse.Presence);
import Presence from 'discourse/mixins/presence';
Discourse.Model.reopenClass({
const Model = Ember.Object.extend(Presence);
Model.reopenClass({
extractByKey: function(collection, klass) {
var retval = {};
if (Ember.isEmpty(collection)) { return retval; }
@ -11,3 +13,5 @@ Discourse.Model.reopenClass({
return retval;
}
});
export default Model;

View File

@ -1,4 +1,6 @@
const PostStream = Ember.Object.extend({
import RestModel from 'discourse/models/rest';
const PostStream = RestModel.extend({
loading: Em.computed.or('loadingAbove', 'loadingBelow', 'loadingFilter', 'stagingPost'),
notLoading: Em.computed.not('loading'),
filteredPostsCount: Em.computed.alias("stream.length"),
@ -148,12 +150,16 @@ const PostStream = Ember.Object.extend({
opts = opts || {};
opts.nearPost = parseInt(opts.nearPost, 10);
const topic = this.get('topic'),
self = this;
const topic = this.get('topic');
const self = this;
// Do we already have the post in our list of posts? Jump there.
const postWeWant = this.get('posts').findProperty('post_number', opts.nearPost);
if (postWeWant) { return Ember.RSVP.resolve(); }
if (opts.forceLoad) {
this.set('loaded', false);
} else {
const postWeWant = this.get('posts').findProperty('post_number', opts.nearPost);
if (postWeWant) { return Ember.RSVP.resolve(); }
}
// TODO: if we have all the posts in the filter, don't go to the server for them.
self.set('loadingFilter', true);
@ -420,8 +426,9 @@ const PostStream = Ember.Object.extend({
} else {
// need to insert into stream
const url = "/posts/" + postId;
const store = this.store;
Discourse.ajax(url).then(function(p){
const post = Discourse.Post.create(p);
const post = store.createRecord('post', p);
const stream = self.get("stream");
const posts = self.get("posts");
self.storePost(post);
@ -461,9 +468,10 @@ const PostStream = Ember.Object.extend({
if(existing){
const url = "/posts/" + postId;
const store = this.store;
Discourse.ajax(url).then(
function(p){
self.storePost(Discourse.Post.create(p));
self.storePost(store.createRecord('post', p));
},
function(){
self.removePosts([existing]);
@ -480,8 +488,9 @@ const PostStream = Ember.Object.extend({
if (existing && existing.updated_at !== updatedAt) {
const url = "/posts/" + postId;
const store = this.store;
Discourse.ajax(url).then(function(p){
self.storePost(Discourse.Post.create(p));
self.storePost(store.createRecord('post', p));
});
}
},
@ -491,9 +500,10 @@ const PostStream = Ember.Object.extend({
const postStream = this,
url = "/posts/" + post.get('id') + "/reply-history.json?max_replies=" + Discourse.SiteSettings.max_reply_history;
const store = this.store;
return Discourse.ajax(url).then(function(result) {
return result.map(function (p) {
return postStream.storePost(Discourse.Post.create(p));
return postStream.storePost(store.createRecord('post', p));
});
}).then(function (replyHistory) {
post.set('replyHistory', replyHistory);
@ -594,8 +604,9 @@ const PostStream = Ember.Object.extend({
this.set('gaps', null);
if (postStreamData) {
// Load posts if present
const store = this.store;
postStreamData.posts.forEach(function(p) {
postStream.appendPost(Discourse.Post.create(p));
postStream.appendPost(store.createRecord('post', p));
});
delete postStreamData.posts;
@ -671,11 +682,12 @@ const PostStream = Ember.Object.extend({
data = { post_ids: postIds },
postStream = this;
const store = this.store;
return Discourse.ajax(url, {data: data}).then(function(result) {
const posts = Em.get(result, "post_stream.posts");
if (posts) {
posts.forEach(function (p) {
postStream.storePost(Discourse.Post.create(p));
postStream.storePost(store.createRecord('post', p));
});
}
});
@ -751,6 +763,8 @@ PostStream.reopenClass({
url += "/" + opts.nearPost;
}
delete opts.nearPost;
delete opts.__type;
delete opts.store;
return PreloadStore.getAndRemove("topic_" + topicId, function() {
return Discourse.ajax(url + ".json", {data: opts});

View File

@ -1,20 +1,15 @@
/**
A data model representing a post in a topic
import RestModel from 'discourse/models/rest';
import { popupAjaxError } from 'discourse/lib/ajax-error';
@class Post
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.Post = Discourse.Model.extend({
const Post = RestModel.extend({
init: function() {
init() {
this.set('replyHistory', []);
},
shareUrl: function() {
var user = Discourse.User.current();
var userSuffix = user ? '?u=' + user.get('username_lower') : '';
const user = Discourse.User.current();
const userSuffix = user ? '?u=' + user.get('username_lower') : '';
if (this.get('firstPost')) {
return this.get('topic.url') + userSuffix;
@ -33,7 +28,7 @@ Discourse.Post = Discourse.Model.extend({
userDeleted: Em.computed.empty('user_id'),
showName: function() {
var name = this.get('name');
const name = this.get('name');
return name && (name !== this.get('username')) && Discourse.SiteSettings.display_name_on_posts;
}.property('name', 'username'),
@ -69,29 +64,23 @@ Discourse.Post = Discourse.Model.extend({
}.property("user_id"),
wikiChanged: function() {
var data = { wiki: this.get("wiki") };
const data = { wiki: this.get("wiki") };
this._updatePost("wiki", data);
}.observes('wiki'),
postTypeChanged: function () {
var data = { post_type: this.get("post_type") };
const data = { post_type: this.get("post_type") };
this._updatePost("post_type", data);
}.observes("post_type"),
_updatePost: function (field, data) {
var self = this;
_updatePost(field, data) {
const self = this;
Discourse.ajax("/posts/" + this.get("id") + "/" + field, {
type: "PUT",
data: data
}).then(function () {
self.incrementProperty("version");
}, function (error) {
if (error && error.responseText) {
bootbox.alert($.parseJSON(error.responseText).errors[0]);
} else {
bootbox.alert(I18n.t("generic_error"));
}
});
}).catch(popupAjaxError);
},
internalLinks: function() {
@ -103,7 +92,7 @@ Discourse.Post = Discourse.Model.extend({
editCount: function() { return this.get('version') - 1; }.property('version'),
flagsAvailable: function() {
var post = this;
const post = this;
return Discourse.Site.currentProp('flagTypes').filter(function(item) {
return post.get("actionByName." + item.get('name_key') + ".can_act");
});
@ -119,73 +108,46 @@ Discourse.Post = Discourse.Model.extend({
});
}.property('actions_summary.@each.users', 'actions_summary.@each.count'),
// Save a post and call the callback when done.
save: function(complete, error) {
var self = this;
if (!this.get('newPost')) {
// We're updating a post
return Discourse.ajax("/posts/" + (this.get('id')), {
type: 'PUT',
dataType: 'json',
data: {
post: { raw: this.get('raw'), edit_reason: this.get('editReason') },
image_sizes: this.get('imageSizes')
}
}).then(function(result) {
// If we received a category update, update it
self.set('version', result.post.version);
if (result.category) Discourse.Site.current().updateCategory(result.category);
if (complete) complete(Discourse.Post.create(result.post));
}).catch(function(result) {
// Post failed to update
if (error) error(result);
});
} else {
// We're saving a post
var data = this.getProperties(Discourse.Composer.serializedFieldsForCreate());
data.reply_to_post_number = this.get('reply_to_post_number');
data.image_sizes = this.get('imageSizes');
var metaData = this.get('metaData');
// Put the metaData into the request
if (metaData) {
data.meta_data = {};
Ember.keys(metaData).forEach(function(key) { data.meta_data[key] = metaData.get(key); });
}
return Discourse.ajax("/posts", {
type: 'POST',
data: data
}).then(function(result) {
// Post created
if (complete) complete(Discourse.Post.create(result));
}).catch(function(result) {
// Failed to create a post
if (error) error(result);
});
afterUpdate(res) {
if (res.category) {
Discourse.Site.current().updateCategory(res.category);
}
},
/**
Expands the first post's content, if embedded and shortened.
updateProperties() {
return {
post: { raw: this.get('raw'), edit_reason: this.get('editReason') },
image_sizes: this.get('imageSizes')
};
},
@method expandFirstPost
**/
expand: function() {
var self = this;
createProperties() {
const data = this.getProperties(Discourse.Composer.serializedFieldsForCreate());
data.reply_to_post_number = this.get('reply_to_post_number');
data.image_sizes = this.get('imageSizes');
const metaData = this.get('metaData');
// Put the metaData into the request
if (metaData) {
data.meta_data = {};
Ember.keys(metaData).forEach(function(key) { data.meta_data[key] = metaData.get(key); });
}
return data;
},
// Expands the first post's content, if embedded and shortened.
expand() {
const self = this;
return Discourse.ajax("/posts/" + this.get('id') + "/expand-embed").then(function(post) {
self.set('cooked', "<section class='expanded-embed'>" + post.cooked + "</section>" );
});
},
/**
Recover a deleted post
@method recover
**/
recover: function() {
var post = this;
// Recover a deleted post
recover() {
const post = this;
post.setProperties({
deleted_at: null,
deleted_by: null,
@ -207,11 +169,8 @@ Discourse.Post = Discourse.Model.extend({
/**
Changes the state of the post to be deleted. Does not call the server, that should be
done elsewhere.
@method setDeletedState
@param {Discourse.User} deletedBy The user deleting the post
**/
setDeletedState: function(deletedBy) {
setDeletedState(deletedBy) {
this.set('oldCooked', this.get('cooked'));
// Moderators can delete posts. Users can only trigger a deleted at message, unless delete_removed_posts_after is 0.
@ -237,10 +196,8 @@ Discourse.Post = Discourse.Model.extend({
Changes the state of the post to NOT be deleted. Does not call the server.
This can only be called after setDeletedState was called, but the delete
failed on the server.
@method undoDeletedState
**/
undoDeleteState: function() {
undoDeleteState() {
if (this.get('oldCooked')) {
this.setProperties({
deleted_at: null,
@ -253,13 +210,7 @@ Discourse.Post = Discourse.Model.extend({
}
},
/**
Deletes a post
@method destroy
@param {Discourse.User} deletedBy The user deleting the post
**/
destroy: function(deletedBy) {
destroy(deletedBy) {
this.setDeletedState(deletedBy);
return Discourse.ajax("/posts/" + this.get('id'), {
data: { context: window.location.pathname },
@ -270,14 +221,11 @@ Discourse.Post = Discourse.Model.extend({
/**
Updates a post from another's attributes. This will normally happen when a post is loading but
is already found in an identity map.
@method updateFromPost
@param {Discourse.Post} otherPost The post we're updating from
**/
updateFromPost: function(otherPost) {
var self = this;
updateFromPost(otherPost) {
const self = this;
Object.keys(otherPost).forEach(function (key) {
var value = otherPost[key],
let value = otherPost[key],
oldValue = self[key];
if (key === "replyHistory") {
@ -287,7 +235,7 @@ Discourse.Post = Discourse.Model.extend({
if (!value) { value = null; }
if (!oldValue) { oldValue = null; }
var skip = false;
let skip = false;
if (typeof value !== "function" && oldValue !== value) {
// wishing for an identity map
if (key === "reply_to_user" && value && oldValue) {
@ -301,56 +249,8 @@ Discourse.Post = Discourse.Model.extend({
});
},
/**
Updates a post from a JSON packet. This is normally done after the post is saved to refresh any
attributes.
@method updateFromJson
@param {Object} obj The Json data to update with
**/
updateFromJson: function(obj) {
if (!obj) return;
var skip, oldVal;
// Update all the properties
var post = this;
_.each(obj, function(val,key) {
if (key !== 'actions_summary'){
oldVal = post[key];
skip = false;
if (val && val !== oldVal) {
if (key === "reply_to_user" && val && oldVal) {
skip = val.username === oldVal.username || Em.get(val, "username") === Em.get(oldVal, "username");
}
if(!skip) {
post.set(key, val);
}
}
}
});
// Rebuild actions summary
this.set('actions_summary', Em.A());
if (obj.actions_summary) {
var lookup = Em.Object.create();
_.each(obj.actions_summary,function(a) {
var actionSummary;
a.post = post;
a.actionType = Discourse.Site.current().postActionTypeById(a.id);
actionSummary = Discourse.ActionSummary.create(a);
post.get('actions_summary').pushObject(actionSummary);
lookup.set(a.actionType.get('name_key'), actionSummary);
});
this.set('actionByName', lookup);
}
},
// Load replies to this post
loadReplies: function() {
loadReplies() {
if(this.get('loadingReplies')){
return;
}
@ -358,12 +258,12 @@ Discourse.Post = Discourse.Model.extend({
this.set('loadingReplies', true);
this.set('replies', []);
var self = this;
const self = this;
return Discourse.ajax("/posts/" + (this.get('id')) + "/replies")
.then(function(loaded) {
var replies = self.get('replies');
const replies = self.get('replies');
_.each(loaded,function(reply) {
var post = Discourse.Post.create(reply);
const post = Discourse.Post.create(reply);
post.set('topic', self.get('topic'));
replies.pushObject(post);
});
@ -375,7 +275,7 @@ Discourse.Post = Discourse.Model.extend({
// Whether to show replies directly below
showRepliesBelow: function() {
var replyCount = this.get('reply_count');
const replyCount = this.get('reply_count');
// We don't show replies if there aren't any
if (replyCount === 0) return false;
@ -387,13 +287,13 @@ Discourse.Post = Discourse.Model.extend({
if (replyCount > 1) return true;
// If we have *exactly* one reply, we have to consider if it's directly below us
var topic = this.get('topic');
const topic = this.get('topic');
return !topic.isReplyDirectlyBelow(this);
}.property('reply_count'),
expandHidden: function() {
var self = this;
expandHidden() {
const self = this;
return Discourse.ajax("/posts/" + this.get('id') + "/cooked.json").then(function (result) {
self.setProperties({
cooked: result.cooked,
@ -402,17 +302,17 @@ Discourse.Post = Discourse.Model.extend({
});
},
rebake: function () {
rebake() {
return Discourse.ajax("/posts/" + this.get("id") + "/rebake", { type: "PUT" });
},
unhide: function () {
unhide() {
return Discourse.ajax("/posts/" + this.get("id") + "/unhide", { type: "PUT" });
},
toggleBookmark: function() {
var self = this,
bookmarkedTopic;
toggleBookmark() {
const self = this;
let bookmarkedTopic;
this.toggleProperty("bookmarked");
@ -432,43 +332,46 @@ Discourse.Post = Discourse.Model.extend({
if (bookmarkedTopic) {self.set("topic.bookmarked", false); }
throw e;
});
},
updateActionsSummary(json) {
if (json && json.id === this.get('id')) {
json = Post.munge(json);
this.set('actions_summary', json.actions_summary);
}
}
});
Discourse.Post.reopenClass({
Post.reopenClass({
createActionSummary: function(result) {
if (result.actions_summary) {
var lookup = Em.Object.create();
munge(json) {
if (json.actions_summary) {
const lookup = Em.Object.create();
// this area should be optimized, it is creating way too many objects per post
result.actions_summary = result.actions_summary.map(function(a) {
a.post = result;
json.actions_summary = json.actions_summary.map(function(a) {
a.actionType = Discourse.Site.current().postActionTypeById(a.id);
var actionSummary = Discourse.ActionSummary.create(a);
const actionSummary = Discourse.ActionSummary.create(a);
lookup[a.actionType.name_key] = actionSummary;
return actionSummary;
});
result.set('actionByName', lookup);
json.actionByName = lookup;
}
if (json && json.reply_to_user) {
json.reply_to_user = Discourse.User.create(json.reply_to_user);
}
return json;
},
create: function(obj) {
var result = this._super.apply(this, arguments);
this.createActionSummary(result);
if (obj && obj.reply_to_user) {
result.set('reply_to_user', Discourse.User.create(obj.reply_to_user));
}
return result;
},
updateBookmark: function(postId, bookmarked) {
updateBookmark(postId, bookmarked) {
return Discourse.ajax("/posts/" + postId + "/bookmark", {
type: 'PUT',
data: { bookmarked: bookmarked }
});
},
deleteMany: function(selectedPosts, selectedReplies) {
deleteMany(selectedPosts, selectedReplies) {
return Discourse.ajax("/posts/destroy_many", {
type: 'DELETE',
data: {
@ -478,37 +381,33 @@ Discourse.Post.reopenClass({
});
},
loadRevision: function(postId, version) {
loadRevision(postId, version) {
return Discourse.ajax("/posts/" + postId + "/revisions/" + version + ".json").then(function (result) {
return Ember.Object.create(result);
});
},
hideRevision: function(postId, version) {
hideRevision(postId, version) {
return Discourse.ajax("/posts/" + postId + "/revisions/" + version + "/hide", { type: 'PUT' });
},
showRevision: function(postId, version) {
showRevision(postId, version) {
return Discourse.ajax("/posts/" + postId + "/revisions/" + version + "/show", { type: 'PUT' });
},
loadQuote: function(postId) {
loadQuote(postId) {
return Discourse.ajax("/posts/" + postId + ".json").then(function (result) {
var post = Discourse.Post.create(result);
const post = Discourse.Post.create(result);
return Discourse.Quote.build(post, post.get('raw'));
});
},
loadRawEmail: function(postId) {
loadRawEmail(postId) {
return Discourse.ajax("/posts/" + postId + "/raw-email").then(function (result) {
return result.raw_email;
});
},
load: function(postId) {
return Discourse.ajax("/posts/" + postId + ".json").then(function (result) {
return Discourse.Post.create(result);
});
}
});
export default Post;

View File

@ -1,16 +1,69 @@
export default Ember.Object.extend({
update(attrs) {
const self = this,
type = this.get('__type');
return this.store.update(type, this.get('id'), attrs).then(function(result) {
if (result && result[type]) {
Object.keys(result).forEach(function(k) {
attrs[k] = result[k];
});
import Presence from 'discourse/mixins/presence';
const RestModel = Ember.Object.extend(Presence, {
isNew: Ember.computed.equal('__state', 'new'),
isCreated: Ember.computed.equal('__state', 'created'),
isSaving: false,
afterUpdate: Ember.K,
update(props) {
if (this.get('isSaving')) { return Ember.RSVP.reject(); }
props = props || this.updateProperties();
const type = this.get('__type'),
store = this.get('store');
const self = this;
self.set('isSaving', true);
return store.update(type, this.get('id'), props).then(function(res) {
const payload = self.__munge(res.payload || res.responseJson);
if (payload.success === "OK") {
Ember.warn("An update call should return the updated attributes");
res = props;
}
self.setProperties(attrs);
return result;
});
self.setProperties(payload);
self.afterUpdate(res);
return res;
}).finally(() => this.set('isSaving', false));
},
_saveNew(props) {
if (this.get('isSaving')) { return Ember.RSVP.reject(); }
props = props || this.createProperties();
const type = this.get('__type'),
store = this.get('store'),
adapter = store.adapterFor(type);
const self = this;
self.set('isSaving', true);
return adapter.createRecord(store, type, props).then(function(res) {
if (!res) { throw "Received no data back from createRecord"; }
// We can get a response back without properties, for example
// when a post is queued.
if (res.payload) {
self.setProperties(self.__munge(res.payload));
self.set('__state', 'created');
}
res.target = self;
return res;
}).finally(() => this.set('isSaving', false));
},
createProperties() {
throw "You must overwrite `createProperties()` before saving a record";
},
save(props) {
return this.get('isNew') ? this._saveNew(props) : this.update(props);
},
destroyRecord() {
@ -18,3 +71,25 @@ export default Ember.Object.extend({
return this.store.destroyRecord(type, this);
}
});
RestModel.reopenClass({
// Overwrite and JSON will be passed through here before `create` and `update`
munge(json) {
return json;
},
create(args) {
args = args || {};
if (!args.store) {
const container = Discourse.__container__;
// Ember.warn('Use `store.createRecord` to create records instead of `.create()`');
args.store = container.lookup('store:main');
}
args.__munge = this.munge;
return this._super(this.munge(args, args.store));
}
});
export default RestModel;

View File

@ -1,7 +1,40 @@
import RestModel from 'discourse/models/rest';
import ResultSet from 'discourse/models/result-set';
const _identityMap = {};
let _identityMap;
// You should only call this if you're a test scaffold
function flushMap() {
_identityMap = {};
}
function storeMap(type, id, obj) {
if (!id) { return; }
_identityMap[type] = _identityMap[type] || {};
_identityMap[type][id] = obj;
}
function fromMap(type, id) {
const byType = _identityMap[type];
if (byType) { return byType[id]; }
}
function removeMap(type, id) {
const byType = _identityMap[type];
if (byType) { delete byType[id]; }
}
function findAndRemoveMap(type, id) {
const byType = _identityMap[type];
if (byType) {
const result = byType[id];
delete byType[id];
return result;
}
}
flushMap();
export default Ember.Object.extend({
pluralize(thing) {
@ -9,21 +42,27 @@ export default Ember.Object.extend({
},
findAll(type) {
const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
const self = this;
return adapter.findAll(this, type).then(function(result) {
return this.adapterFor(type).findAll(this, type).then(function(result) {
return self._resultSet(type, result);
});
},
find(type, findArgs) {
const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
// Mostly for legacy, things like TopicList without ResultSets
findFiltered(type, findArgs) {
const self = this;
return adapter.find(this, type, findArgs).then(function(result) {
return this.adapterFor(type).find(this, type, findArgs).then(function(result) {
return self._build(type, result);
});
},
find(type, findArgs) {
const self = this;
return this.adapterFor(type).find(this, type, findArgs).then(function(result) {
if (typeof findArgs === "object") {
return self._resultSet(type, result);
} else {
return self._hydrate(type, result[Ember.String.underscore(type)]);
return self._hydrate(type, result[Ember.String.underscore(type)], result);
}
});
},
@ -35,7 +74,7 @@ export default Ember.Object.extend({
const typeName = Ember.String.underscore(self.pluralize(type)),
totalRows = result["total_rows_" + typeName] || result.get('totalRows'),
loadMoreUrl = result["load_more_" + typeName],
content = result[typeName].map(obj => self._hydrate(type, obj));
content = result[typeName].map(obj => self._hydrate(type, obj, result));
resultSet.setProperties({ totalRows, loadMoreUrl });
resultSet.get('content').pushObjects(content);
@ -48,58 +87,120 @@ export default Ember.Object.extend({
},
update(type, id, attrs) {
const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
return adapter.update(this, type, id, attrs, function(result) {
return this.adapterFor(type).update(this, type, id, attrs, function(result) {
if (result && result[type] && result[type].id) {
const oldRecord = _identityMap[type][id];
delete _identityMap[type][id];
_identityMap[type][result[type].id] = oldRecord;
const oldRecord = findAndRemoveMap(type, id);
storeMap(type, result[type].id, oldRecord);
}
return result;
});
},
createRecord(type, attrs) {
return this._hydrate(type, attrs);
attrs = attrs || {};
return !!attrs.id ? this._hydrate(type, attrs) : this._build(type, attrs);
},
destroyRecord(type, record) {
const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
return adapter.destroyRecord(this, type, record).then(function(result) {
const forType = _identityMap[type];
if (forType) { delete forType[record.get('id')]; }
return this.adapterFor(type).destroyRecord(this, type, record).then(function(result) {
removeMap(type, record.get('id'));
return result;
});
},
_resultSet(type, result) {
const typeName = Ember.String.underscore(this.pluralize(type)),
content = result[typeName].map(obj => this._hydrate(type, obj)),
content = result[typeName].map(obj => this._hydrate(type, obj, result)),
totalRows = result["total_rows_" + typeName] || content.length,
loadMoreUrl = result["load_more_" + typeName];
return ResultSet.create({ content, totalRows, loadMoreUrl, store: this, __type: type });
},
_hydrate(type, obj) {
if (!obj) { throw "Can't hydrate " + type + " of `null`"; }
if (!obj.id) { throw "Can't hydrate " + type + " without an `id`"; }
_identityMap[type] = _identityMap[type] || {};
const existing = _identityMap[type][obj.id];
if (existing) {
delete obj.id;
existing.setProperties(obj);
return existing;
}
_build(type, obj) {
obj.store = this;
obj.__type = type;
obj.__state = obj.id ? "created" : "new";
const klass = this.container.lookupFactory('model:' + type) || RestModel;
const model = klass.create(obj);
_identityMap[type][obj.id] = model;
storeMap(type, obj.id, model);
return model;
},
adapterFor(type) {
return this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
},
_lookupSubType(subType, id, root) {
// cheat: we know we already have categories in memory
if (subType === 'category') {
return Discourse.Category.findById(id);
}
const pluralType = this.pluralize(subType);
const collection = root[this.pluralize(subType)];
if (collection) {
const hashedProp = "__hashed_" + pluralType;
let hashedCollection = root[hashedProp];
if (!hashedCollection) {
hashedCollection = {};
collection.forEach(function(it) {
hashedCollection[it.id] = it;
});
root[hashedProp] = hashedCollection;
}
const found = hashedCollection[id];
if (found) {
const hydrated = this._hydrate(subType, found, root);
hashedCollection[id] = hydrated;
return hydrated;
}
}
},
_hydrateEmbedded(obj, root) {
const self = this;
Object.keys(obj).forEach(function(k) {
const m = /(.+)\_id$/.exec(k);
if (m) {
const subType = m[1];
const hydrated = self._lookupSubType(subType, obj[k], root);
if (hydrated) {
obj[subType] = hydrated;
delete obj[k];
}
}
});
},
_hydrate(type, obj, root) {
if (!obj) { throw "Can't hydrate " + type + " of `null`"; }
if (!obj.id) { throw "Can't hydrate " + type + " without an `id`"; }
root = root || obj;
// Experimental: If serialized with a certain option we'll wire up embedded objects
// automatically.
if (root.__rest_serializer === "1") {
this._hydrateEmbedded(obj, root);
}
const existing = fromMap(type, obj.id);
if (existing === obj) { return existing; }
if (existing) {
delete obj.id;
const klass = this.container.lookupFactory('model:' + type) || RestModel;
existing.setProperties(klass.munge(obj));
return existing;
}
return this._build(type, obj);
}
});
export { flushMap };

View File

@ -2,7 +2,9 @@
A model representing a Topic's details that aren't always present, such as a list of participants.
When showing topics in lists and such this information should not be required.
**/
const TopicDetails = Discourse.Model.extend({
import RestModel from 'discourse/models/rest';
const TopicDetails = RestModel.extend({
loaded: false,
updateFromJson(details) {
@ -15,8 +17,9 @@ const TopicDetails = Discourse.Model.extend({
}
if (details.suggested_topics) {
const store = this.store;
details.suggested_topics = details.suggested_topics.map(function (st) {
return Discourse.Topic.create(st);
return store.createRecord('topic', st);
});
}

View File

@ -0,0 +1,168 @@
import RestModel from 'discourse/models/rest';
import Model from 'discourse/models/model';
function topicsFrom(result, store) {
if (!result) { return; }
// Stitch together our side loaded data
const categories = Discourse.Category.list(),
users = Model.extractByKey(result.users, Discourse.User);
return result.topic_list.topics.map(function (t) {
t.category = categories.findBy('id', t.category_id);
t.posters.forEach(function(p) {
p.user = users[p.user_id];
});
if (t.participants) {
t.participants.forEach(function(p) {
p.user = users[p.user_id];
});
}
return store.createRecord('topic', t);
});
}
const TopicList = RestModel.extend({
canLoadMore: Em.computed.notEmpty("more_topics_url"),
forEachNew: function(topics, callback) {
const topicIds = [];
_.each(this.get('topics'),function(topic) {
topicIds[topic.get('id')] = true;
});
_.each(topics,function(topic) {
if(!topicIds[topic.id]) {
callback(topic);
}
});
},
refreshSort: function(order, ascending) {
const self = this,
params = this.get('params');
params.order = order || params.order;
if (ascending === undefined) {
params.ascending = ascending;
} else {
params.ascending = ascending;
}
this.set('loaded', false);
const store = this.store;
store.findFiltered('topicList', {filter: this.get('filter'), params}).then(function(tl) {
const newTopics = tl.get('topics'),
topics = self.get('topics');
topics.clear();
topics.pushObjects(newTopics);
self.setProperties({ loaded: true, more_topics_url: newTopics.get('more_topics_url') });
});
},
loadMore: function() {
if (this.get('loadingMore')) { return Ember.RSVP.resolve(); }
const moreUrl = this.get('more_topics_url');
if (moreUrl) {
const self = this;
this.set('loadingMore', true);
const store = this.store;
return Discourse.ajax({url: moreUrl}).then(function (result) {
let topicsAdded = 0;
if (result) {
// the new topics loaded from the server
const newTopics = topicsFrom(result, store),
topics = self.get("topics");
self.forEachNew(newTopics, function(t) {
t.set('highlight', topicsAdded++ === 0);
topics.pushObject(t);
});
self.setProperties({
loadingMore: false,
more_topics_url: result.topic_list.more_topics_url
});
Discourse.Session.currentProp('topicList', self);
return self.get('more_topics_url');
}
});
} else {
// Return a promise indicating no more results
return Ember.RSVP.resolve();
}
},
// loads topics with these ids "before" the current topics
loadBefore: function(topic_ids){
const topicList = this,
topics = this.get('topics');
// refresh dupes
topics.removeObjects(topics.filter(function(topic){
return topic_ids.indexOf(topic.get('id')) >= 0;
}));
const url = Discourse.getURL("/") + this.get('filter') + "?topic_ids=" + topic_ids.join(",");
const store = this.store;
return Discourse.ajax({ url }).then(function(result) {
let i = 0;
topicList.forEachNew(topicsFrom(result, store), function(t) {
// highlight the first of the new topics so we can get a visual feedback
t.set('highlight', true);
topics.insertAt(i,t);
i++;
});
Discourse.Session.currentProp('topicList', topicList);
});
}
});
TopicList.reopenClass({
munge(json, store) {
json.inserted = json.inserted || [];
json.can_create_topic = json.topic_list.can_create_topic;
json.more_topics_url = json.topic_list.more_topics_url;
json.draft_key = json.topic_list.draft_key;
json.draft_sequence = json.topic_list.draft_sequence;
json.draft = json.topic_list.draft;
json.for_period = json.topic_list.for_period;
json.loaded = true;
json.per_page = json.topic_list.per_page;
json.topics = topicsFrom(json, store);
if (json.topic_list.filtered_category) {
json.category = Discourse.Category.create(json.topic_list.filtered_category);
}
return json;
},
find(filter, params) {
const store = Discourse.__container__.lookup('store:main');
return store.findFiltered('topicList', {filter, params});
},
list(filter) {
Ember.warn('`Discourse.TopicList.list` is deprecated. Use the store instead');
return this.find(filter);
},
// Sets `hideCategory` if all topics in the last have a particular category
hideUniformCategory(list, category) {
const hideCategory = !list.get('topics').any(function (t) { return t.get('category') !== category; });
list.set('hideCategory', hideCategory);
}
});
export default TopicList;

View File

@ -1,7 +1,6 @@
import TopicDetails from 'discourse/models/topic-details';
import PostStream from 'discourse/models/post-stream';
import RestModel from 'discourse/models/rest';
const Topic = Discourse.Model.extend({
const Topic = RestModel.extend({
// returns createdAt if there's no bumped date
bumpedAt: function() {
@ -23,7 +22,7 @@ const Topic = Discourse.Model.extend({
}.property('created_at'),
postStream: function() {
return PostStream.create({topic: this});
return this.store.createRecord('postStream', {id: this.get('id'), topic: this});
}.property(),
replyCount: function() {
@ -31,7 +30,7 @@ const Topic = Discourse.Model.extend({
}.property('posts_count'),
details: function() {
return TopicDetails.create({topic: this});
return this.store.createRecord('topicDetails', {id: this.get('id'), topic: this});
}.property(),
invisible: Em.computed.not('visible'),
@ -41,18 +40,18 @@ const Topic = Discourse.Model.extend({
return ({ type: 'topic', id: this.get('id') });
}.property('id'),
category: function() {
const categoryId = this.get('category_id');
if (categoryId) {
return Discourse.Category.list().findProperty('id', categoryId);
}
_categoryIdChanged: function() {
this.set('category', Discourse.Category.findById(this.get('category_id')));
}.observes('category_id').on('init'),
_categoryNameChanged: function() {
const categoryName = this.get('categoryName');
let category;
if (categoryName) {
return Discourse.Category.list().findProperty('name', categoryName);
category = Discourse.Category.list().findProperty('name', categoryName);
}
return null;
}.property('category_id', 'categoryName'),
this.set('category', category);
}.observes('categoryName'),
categoryClass: function() {
return 'category-' + this.get('category.fullSlug');
@ -408,7 +407,6 @@ Topic.reopenClass({
// The title can be cleaned up server side
props.title = result.basic_topic.title;
props.fancy_title = result.basic_topic.fancy_title;
topic.setProperties(props);
});
},

View File

@ -1,272 +0,0 @@
function finderFor(filter, params) {
return function() {
var url = Discourse.getURL("/") + filter + ".json";
if (params) {
var keys = Object.keys(params),
encoded = [];
keys.forEach(function(p) {
var value = params[p];
if (typeof value !== 'undefined') {
encoded.push(p + "=" + value);
}
});
if (encoded.length > 0) {
url += "?" + encoded.join('&');
}
}
return Discourse.ajax(url);
};
}
Discourse.TopicList = Discourse.Model.extend({
canLoadMore: Em.computed.notEmpty("more_topics_url"),
forEachNew: function(topics, callback) {
var topicIds = [];
_.each(this.get('topics'),function(topic) {
topicIds[topic.get('id')] = true;
});
_.each(topics,function(topic) {
if(!topicIds[topic.id]) {
callback(topic);
}
});
},
refreshSort: function(order, ascending) {
var self = this,
params = this.get('params');
params.order = order || params.order;
if (ascending === undefined) {
params.ascending = ascending;
} else {
params.ascending = ascending;
}
this.set('loaded', false);
var finder = finderFor(this.get('filter'), params);
finder().then(function (result) {
var newTopics = Discourse.TopicList.topicsFrom(result),
topics = self.get('topics');
topics.clear();
topics.pushObjects(newTopics);
self.setProperties({ loaded: true, more_topics_url: result.topic_list.more_topics_url });
});
},
loadMore: function() {
if (this.get('loadingMore')) { return Ember.RSVP.resolve(); }
var moreUrl = this.get('more_topics_url');
if (moreUrl) {
var self = this;
this.set('loadingMore', true);
return Discourse.ajax({url: moreUrl}).then(function (result) {
var topicsAdded = 0;
if (result) {
// the new topics loaded from the server
var newTopics = Discourse.TopicList.topicsFrom(result),
topics = self.get("topics");
self.forEachNew(newTopics, function(t) {
t.set('highlight', topicsAdded++ === 0);
topics.pushObject(t);
});
self.setProperties({
loadingMore: false,
more_topics_url: result.topic_list.more_topics_url
});
Discourse.Session.currentProp('topicList', self);
return self.get('more_topics_url');
}
});
} else {
// Return a promise indicating no more results
return Ember.RSVP.resolve();
}
},
// loads topics with these ids "before" the current topics
loadBefore: function(topic_ids){
var topicList = this,
topics = this.get('topics');
// refresh dupes
topics.removeObjects(topics.filter(function(topic){
return topic_ids.indexOf(topic.get('id')) >= 0;
}));
Discourse.TopicList.loadTopics(topic_ids, this.get('filter'))
.then(function(newTopics){
var i = 0;
topicList.forEachNew(newTopics, function(t) {
// highlight the first of the new topics so we can get a visual feedback
t.set('highlight', true);
topics.insertAt(i,t);
i++;
});
Discourse.Session.currentProp('topicList', topicList);
});
}
});
Discourse.TopicList.reopenClass({
loadTopics: function(topic_ids, filter) {
return new Ember.RSVP.Promise(function(resolve, reject) {
var url = Discourse.getURL("/") + filter + "?topic_ids=" + topic_ids.join(",");
Discourse.ajax({url: url}).then(function (result) {
if (result) {
// the new topics loaded from the server
var newTopics = Discourse.TopicList.topicsFrom(result);
resolve(newTopics);
} else {
reject();
}
}).catch(reject);
});
},
/**
Stitch together side loaded topic data
@method topicsFrom
@param {Object} result JSON object with topic data
@returns {Array} the list of topics
**/
topicsFrom: function(result) {
// Stitch together our side loaded data
var categories = Discourse.Category.list(),
users = this.extractByKey(result.users, Discourse.User);
return result.topic_list.topics.map(function (t) {
t.category = categories.findBy('id', t.category_id);
t.posters.forEach(function(p) {
p.user = users[p.user_id];
});
if (t.participants) {
t.participants.forEach(function(p) {
p.user = users[p.user_id];
});
}
return Discourse.Topic.create(t);
});
},
from: function(result, filter, params) {
var topicList = Discourse.TopicList.create({
inserted: [],
filter: filter,
params: params || {},
topics: Discourse.TopicList.topicsFrom(result),
can_create_topic: result.topic_list.can_create_topic,
more_topics_url: result.topic_list.more_topics_url,
draft_key: result.topic_list.draft_key,
draft_sequence: result.topic_list.draft_sequence,
draft: result.topic_list.draft,
for_period: result.topic_list.for_period,
loaded: true,
per_page: result.topic_list.per_page
});
if (result.topic_list.filtered_category) {
topicList.set('category', Discourse.Category.create(result.topic_list.filtered_category));
}
return topicList;
},
/**
Lists topics on a given menu item
@method list
@param {Object} filter The menu item to filter to
@param {Object} params Any additional params to pass to TopicList.find()
@param {Object} extras Additional finding options, such as caching
@returns {Promise} a promise that resolves to the list of topics
**/
list: function(filter, filterParams, extras) {
var tracking = Discourse.TopicTrackingState.current();
extras = extras || {};
return new Ember.RSVP.Promise(function(resolve) {
var session = Discourse.Session.current();
if (extras.cached) {
var cachedList = session.get('topicList');
// Try to use the cached version if it exists and is greater than the topics per page
if (cachedList && (cachedList.get('filter') === filter) &&
(cachedList.get('topics.length') || 0) > cachedList.get('per_page') &&
_.isEqual(cachedList.get('listParams'), filterParams)) {
cachedList.set('loaded', true);
if (tracking) {
tracking.updateTopics(cachedList.get('topics'));
}
return resolve(cachedList);
}
session.set('topicList', null);
} else {
// Clear the cache
session.setProperties({topicList: null, topicListScrollPosition: null});
}
// Clean up any string parameters that might slip through
filterParams = filterParams || {};
Ember.keys(filterParams).forEach(function(k) {
var val = filterParams[k];
if (val === "undefined" || val === "null" || val === 'false') {
filterParams[k] = undefined;
}
});
var findParams = {};
Discourse.SiteSettings.top_menu.split('|').forEach(function (i) {
if (i.indexOf(filter) === 0) {
var exclude = i.split("-");
if (exclude && exclude.length === 2) {
findParams.exclude_category = exclude[1];
}
}
});
return resolve(Discourse.TopicList.find(filter, _.extend(findParams, filterParams || {})));
}).then(function(list) {
list.set('listParams', filterParams);
if (tracking) {
tracking.sync(list, list.filter);
tracking.trackIncoming(list.filter);
}
Discourse.Session.currentProp('topicList', list);
return list;
});
},
find: function(filter, params) {
return PreloadStore.getAndRemove("topic_list_" + filter, finderFor(filter, params)).then(function(result) {
return Discourse.TopicList.from(result, filter, params);
});
},
// Sets `hideCategory` if all topics in the last have a particular category
hideUniformCategory: function(list, category) {
var hideCategory = !list.get('topics').any(function (t) { return t.get('category') !== category; });
list.set('hideCategory', hideCategory);
}
});

View File

@ -1,6 +1,7 @@
import RestModel from 'discourse/models/rest';
import avatarTemplate from 'discourse/lib/avatar-template';
const User = Discourse.Model.extend({
const User = RestModel.extend({
hasPMs: Em.computed.gt("private_messages_stats.all", 0),
hasStartedPMs: Em.computed.gt("private_messages_stats.mine", 0),

View File

@ -1,12 +1,3 @@
/**
A data model representing actions users have taken
@class UserAction
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
var UserActionTypes = {
likes_given: 1,
likes_received: 2,
@ -18,7 +9,8 @@ var UserActionTypes = {
quotes: 9,
edits: 11,
messages_sent: 12,
messages_received: 13
messages_received: 13,
pending: 14
},
InvertedActionTypes = {};

View File

@ -66,7 +66,7 @@ export default function() {
this.route('flaggedPosts', { path: '/flagged-posts' });
this.route('deletedPosts', { path: '/deleted-posts' });
this.resource('userPrivateMessages', { path: '/private-messages' }, function() {
this.resource('userPrivateMessages', { path: '/messages' }, function() {
this.route('mine');
this.route('unread');
});
@ -93,4 +93,6 @@ export default function() {
this.resource('badges', function() {
this.route('show', {path: '/:id/:slug'});
});
this.resource('queued-posts', { path: '/queued-posts' });
}

View File

@ -37,6 +37,10 @@ const ApplicationRoute = Discourse.Route.extend({
this.controllerFor('topic-entrance').send('show', data);
},
postWasEnqueued() {
showModal('post-enqueued', {title: 'queue.approval.title' });
},
composePrivateMessage(user, post) {
const self = this;
this.transitionTo('userActivity', user).then(function () {
@ -76,12 +80,12 @@ const ApplicationRoute = Discourse.Route.extend({
showCreateAccount: unlessReadOnly('handleShowCreateAccount'),
showForgotPassword() {
showModal('forgotPassword');
showModal('forgotPassword', { title: 'forgot_password.title' });
},
showNotActivated(props) {
showModal('notActivated');
this.controllerFor('notActivated').setProperties(props);
const controller = showModal('not-activated', {title: 'log_in' });
controller.setProperties(props);
},
showUploadSelector(composerView) {
@ -90,13 +94,13 @@ const ApplicationRoute = Discourse.Route.extend({
},
showKeyboardShortcutsHelp() {
showModal('keyboardShortcutsHelp');
showModal('keyboard-shortcuts-help', { title: 'keyboard_shortcuts_help.title'});
},
showSearchHelp() {
// TODO: @EvitTrout how do we get a loading indicator here?
Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then(function(html){
showModal('searchHelp', html);
Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then(function(model){
showModal('searchHelp', { model });
});
},
@ -120,9 +124,9 @@ const ApplicationRoute = Discourse.Route.extend({
editCategory(category) {
const self = this;
Discourse.Category.reloadById(category.get('id')).then(function (c) {
self.site.updateCategory(c);
showModal('editCategory', c);
Discourse.Category.reloadById(category.get('id')).then(function (model) {
self.site.updateCategory(model);
showModal('editCategory', { model });
self.controllerFor('editCategory').set('selectedTab', 'general');
});
},
@ -140,7 +144,7 @@ const ApplicationRoute = Discourse.Route.extend({
const controllerName = w.replace('modal/', ''),
factory = this.container.lookupFactory('controller:' + controllerName);
this.render(w, {into: 'topicBulkActions', outlet: 'bulkOutlet', controller: factory ? controllerName : 'topic-bulk-actions'});
this.render(w, {into: 'modal/topic-bulk-actions', outlet: 'bulkOutlet', controller: factory ? controllerName : 'topic-bulk-actions'});
}
},

View File

@ -1,4 +1,4 @@
import { queryParams, filterQueryParams } from 'discourse/routes/build-topic-route';
import { queryParams, filterQueryParams, findTopicList } from 'discourse/routes/build-topic-route';
// A helper function to create a category route with parameters
export default function(filter, params) {
@ -52,7 +52,7 @@ export default function(filter, params) {
var findOpts = filterQueryParams(transition.queryParams, params),
extras = { cached: this.isPoppedState(transition) };
return Discourse.TopicList.list(listFilter, findOpts, extras).then(function(list) {
return findTopicList(this.store, listFilter, findOpts, extras).then(function(list) {
Discourse.TopicList.hideUniformCategory(list, model);
self.set('topics', list);
});

View File

@ -11,6 +11,65 @@ function filterQueryParams(params, defaultParams) {
return findOpts;
}
function findTopicList(store, filter, filterParams, extras) {
const tracking = Discourse.TopicTrackingState.current();
extras = extras || {};
return new Ember.RSVP.Promise(function(resolve) {
const session = Discourse.Session.current();
if (extras.cached) {
const cachedList = session.get('topicList');
// Try to use the cached version if it exists and is greater than the topics per page
if (cachedList && (cachedList.get('filter') === filter) &&
(cachedList.get('topics.length') || 0) > cachedList.get('per_page') &&
_.isEqual(cachedList.get('listParams'), filterParams)) {
cachedList.set('loaded', true);
if (tracking) {
tracking.updateTopics(cachedList.get('topics'));
}
return resolve(cachedList);
}
session.set('topicList', null);
} else {
// Clear the cache
session.setProperties({topicList: null, topicListScrollPosition: null});
}
// Clean up any string parameters that might slip through
filterParams = filterParams || {};
Ember.keys(filterParams).forEach(function(k) {
const val = filterParams[k];
if (val === "undefined" || val === "null" || val === 'false') {
filterParams[k] = undefined;
}
});
const findParams = {};
Discourse.SiteSettings.top_menu.split('|').forEach(function (i) {
if (i.indexOf(filter) === 0) {
const exclude = i.split("-");
if (exclude && exclude.length === 2) {
findParams.exclude_category = exclude[1];
}
}
});
return resolve(store.findFiltered('topicList', { filter, params:_.extend(findParams, filterParams || {})}));
}).then(function(list) {
list.set('listParams', filterParams);
if (tracking) {
tracking.sync(list, list.filter);
tracking.trackIncoming(list.filter);
}
Discourse.Session.currentProp('topicList', list);
return list;
});
}
export default function(filter, extras) {
extras = extras || {};
return Discourse.Route.extend({
@ -28,7 +87,7 @@ export default function(filter, extras) {
const findOpts = filterQueryParams(transition.queryParams),
extras = { cached: this.isPoppedState(transition) };
return Discourse.TopicList.list(filter, findOpts, extras);
return findTopicList(this.store, filter, findOpts, extras);
},
titleToken() {
@ -72,4 +131,4 @@ export default function(filter, extras) {
}, extras);
}
export { filterQueryParams };
export { filterQueryParams, findTopicList };

View File

@ -14,7 +14,7 @@ export default function (viewName, path) {
},
model: function() {
return Discourse.TopicList.find('topics/' + path + '/' + this.modelFor('user').get('username_lower'));
return this.store.findFiltered('topicList', {filter: 'topics/' + path + '/' + this.modelFor('user').get('username_lower')});
},
setupController: function() {

View File

@ -1,5 +1,3 @@
import showModal from 'discourse/lib/show-modal';
const DiscourseRoute = Ember.Route.extend({
// Set to true to refresh a model without a transition if a query param
@ -210,11 +208,6 @@ DiscourseRoute.reopenClass({
this.route('unknown', {path: '*path'});
});
},
showModal: function(route, name, model) {
Ember.warn('DEPRECATED `Discourse.Route.showModal` - use `showModal` instead');
showModal(name, model);
}
});

View File

@ -46,11 +46,13 @@ Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(Discourse.OpenCompos
const groups = this.site.groups,
everyoneName = groups.findBy('id', 0).name;
showModal('editCategory', Discourse.Category.create({
const model = Discourse.Category.create({
color: 'AB9364', text_color: 'FFFFFF', group_permissions: [{group_name: everyoneName, permission_type: 1}],
available_groups: groups.map(g => g.name),
allow_badges: true
}));
});
showModal('editCategory', { model });
this.controllerFor('editCategory').set('selectedTab', 'general');
},

View File

@ -0,0 +1,7 @@
import DiscourseRoute from 'discourse/routes/discourse';
export default DiscourseRoute.extend({
model() {
return this.store.find('queuedPost', {status: 'new'});
}
});

View File

@ -1,6 +1,9 @@
// This route is used for retrieving a topic based on params
export default Discourse.Route.extend({
// Avoid default model hook
model: function(p) { return p; },
setupController: function(controller, params) {
params = params || {};
params.track_visit = true;
@ -15,6 +18,7 @@ export default Discourse.Route.extend({
if (params.nearPost === "last") { params.nearPost = 999999999; }
var self = this;
params.forceLoad = true;
postStream.refresh(params).then(function () {
// TODO we are seeing errors where closest post is null and this is exploding

View File

@ -5,7 +5,6 @@ let isTransitioning = false,
const SCROLL_DELAY = 500;
import ShowFooter from "discourse/mixins/show-footer";
import Topic from 'discourse/models/topic';
import showModal from 'discourse/lib/show-modal';
const TopicRoute = Discourse.Route.extend(ShowFooter, {
@ -44,52 +43,52 @@ const TopicRoute = Discourse.Route.extend(ShowFooter, {
this.controllerFor("topic-admin-menu").send("show");
},
showFlags(post) {
showModal('flag', post);
showFlags(model) {
showModal('flag', { model });
this.controllerFor('flag').setProperties({ selected: null });
},
showFlagTopic(topic) {
showModal('flag', topic);
showFlagTopic(model) {
showModal('flag', { model });
this.controllerFor('flag').setProperties({ selected: null, flagTopic: true });
},
showAutoClose() {
showModal('editTopicAutoClose', this.modelFor('topic'));
showModal('edit-topic-auto-close', { model: this.modelFor('topic'), title: 'topic.auto_close_title' });
this.controllerFor('modal').set('modalClass', 'edit-auto-close-modal');
},
showFeatureTopic() {
showModal('featureTopic', this.modelFor('topic'));
showModal('featureTopic', { model: this.modelFor('topic'), title: 'topic.feature_topic.title' });
this.controllerFor('modal').set('modalClass', 'feature-topic-modal');
},
showInvite() {
showModal('invite', this.modelFor('topic'));
showModal('invite', { model: this.modelFor('topic') });
this.controllerFor('invite').reset();
},
showHistory(post) {
showModal('history', post);
this.controllerFor('history').refresh(post.get("id"), "latest");
showHistory(model) {
showModal('history', { model });
this.controllerFor('history').refresh(model.get("id"), "latest");
this.controllerFor('modal').set('modalClass', 'history-modal');
},
showRawEmail(post) {
showModal('raw-email', post);
this.controllerFor('raw_email').loadRawEmail(post.get("id"));
showRawEmail(model) {
showModal('raw-email', { model });
this.controllerFor('raw_email').loadRawEmail(model.get("id"));
},
mergeTopic() {
showModal('mergeTopic', this.modelFor('topic'));
showModal('merge-topic', { model: this.modelFor('topic'), title: 'topic.merge_topic.title' });
},
splitTopic() {
showModal('split-topic', this.modelFor('topic'));
showModal('split-topic', { model: this.modelFor('topic') });
},
changeOwner() {
showModal('changeOwner', this.modelFor('topic'));
showModal('change-owner', { model: this.modelFor('topic'), title: 'topic.change_owner.title' });
},
// Use replaceState to update the URL once it changes
@ -153,7 +152,7 @@ const TopicRoute = Discourse.Route.extend(ShowFooter, {
model(params, transition) {
const queryParams = transition.queryParams;
const topic = this.modelFor('topic');
let topic = this.modelFor('topic');
if (topic && (topic.get('id') === parseInt(params.id, 10))) {
this.setupParams(topic, queryParams);
// If we have the existing model, refresh it
@ -161,7 +160,8 @@ const TopicRoute = Discourse.Route.extend(ShowFooter, {
return topic;
});
} else {
return this.setupParams(Topic.create(_.omit(params, 'username_filters', 'filter')), queryParams);
topic = this.store.createRecord('topic', _.omit(params, 'username_filters', 'filter'));
return this.setupParams(topic, queryParams);
}
},

View File

@ -0,0 +1,5 @@
import UserActivityStreamRoute from "discourse/routes/user-activity-stream";
export default UserActivityStreamRoute.extend({
userActionType: Discourse.UserAction.TYPES.pending
});

View File

@ -4,6 +4,6 @@ export default UserTopicListRoute.extend({
userActionType: Discourse.UserAction.TYPES.topics,
model: function() {
return Discourse.TopicList.find('topics/created-by/' + this.modelFor('user').get('username_lower'));
return this.store.findFiltered('topicList', {filter: 'topics/created-by/' + this.modelFor('user').get('username_lower') });
}
});

View File

@ -21,7 +21,7 @@ export default Discourse.Route.extend(ShowFooter, {
actions: {
showInvite() {
showModal('invite', Discourse.User.current());
showModal('invite', { model: this.currentUser });
this.controllerFor('invite').reset();
},

View File

@ -15,33 +15,33 @@
</li>
<li>
<a {{bind-attr href="topic.lastPostUrl"}}>
<h4>{{i18n 'last_post_lowercase'}}</h4>
<h4>{{i18n 'last_reply_lowercase'}}</h4>
{{avatar details.last_poster imageSize="tiny"}}
{{format-date topic.last_posted_at}}
</a>
</li>
<li>
{{number topic.posts_count}}
<h4>{{i18n 'posts_lowercase'}}</h4>
{{number topic.replyCount}}
<h4>{{i18n 'replies_lowercase' count=topic.replyCount}}</h4>
</li>
<li class='secondary'>
{{number topic.views class=topic.viewsHeat}}
<h4>{{i18n 'views_lowercase'}}</h4>
<h4>{{i18n 'views_lowercase' count=topic.views}}</h4>
</li>
<li class='secondary'>
{{number topic.participant_count}}
<h4>{{i18n 'users_lowercase'}}</h4>
<h4>{{i18n 'users_lowercase' count=topic.participant_count}}</h4>
</li>
{{#if topic.like_count}}
<li class='secondary'>
{{number topic.like_count}}
<h4>{{i18n 'likes_lowercase'}}</h4>
<h4>{{i18n 'likes_lowercase' count=topic.like_count}}</h4>
</li>
{{/if}}
{{#if details.links.length}}
<li class='secondary'>
{{number details.links.length}}
<h4>{{i18n 'links_lowercase'}}</h4>
<h4>{{i18n 'links_lowercase' count=details.links.length}}</h4>
</li>
{{/if}}
{{#if showPosterAvatar}}

View File

@ -23,6 +23,7 @@
<div class="row">
<div class="full-width">
<div id='list-area'>
{{plugin-outlet "discovery-list-container-top"}}
{{outlet "list-container"}}
</div>
</div>

View File

@ -10,7 +10,11 @@
{{else}}
<label>{{inviteInstructions}}</label>
{{#if allowExistingMembers}}
{{user-selector single="true" allowAny=true excludeCurrentUser="true" usernames=emailOrUsername includeGroups="true" placeholderKey=placeholderKey}}
{{#if isPrivateTopic}}
{{user-selector single="true" allowAny=true excludeCurrentUser="true" usernames=emailOrUsername allowedUsers="true" topicId=topicId placeholderKey=placeholderKey}}
{{else}}
{{user-selector single="true" allowAny=true excludeCurrentUser="true" usernames=emailOrUsername placeholderKey=placeholderKey}}
{{/if}}
{{else}}
{{text-field value=emailOrUsername placeholderKey="topic.invite_reply.email_placeholder"}}
{{/if}}

View File

@ -0,0 +1,6 @@
<div class="modal-body">
<p>{{i18n "queue.approval.description"}}</p>
</div>
<div class="modal-footer">
{{d-button action="closeModal" class="btn-primary" label="queue.approval.ok"}}
</div>

View File

@ -6,7 +6,7 @@
{{notification-item notification=n scope=n.scope}}
{{/each}}
<li class="read last">
<a href="/my/notifications">{{i18n 'notifications.more'}}&hellip;</a>
<a href="{{unbound myNotificationsUrl}}">{{i18n 'notifications.more'}}&hellip;</a>
</li>
</ul>
{{else}}

View File

@ -1,2 +1,2 @@
<span class="close"><i class="fa fa-times-circle"></i></span>
{{view.validation.reason}}
{{{view.validation.reason}}}

View File

@ -0,0 +1,79 @@
<div class='container'>
<div class='queued-posts'>
{{#each ctrl in model itemController='queued-post'}}
<div class='queued-post'>
<div class='poster'>
{{#user-link user=ctrl.post.user}}
{{avatar ctrl.post.user imageSize="large"}}
{{/user-link}}
</div>
<div class='cooked'>
<div class='names'>
<span class="username">
{{#user-link user=ctrl.post.user}}
{{ctrl.post.user.username}}
{{/user-link}}
</span>
</div>
<div class='post-info'>
<span class='post-date'>{{age-with-tooltip ctrl.post.created_at}}</span>
</div>
<div class='clearfix'></div>
<span class='post-title'>
{{i18n "queue.topic"}}
{{#if ctrl.post.topic}}
{{topic-link ctrl.post.topic}}
{{else}}
{{ctrl.post.post_options.title}}
{{/if}}
{{category-badge ctrl.post.category}}
</span>
<div class='body'>
{{#if ctrl.editing}}
{{pagedown-editor value=ctrl.buffered.raw}}
{{else}}
{{{cook-text ctrl.post.raw}}}
{{/if}}
</div>
<div class='queue-controls'>
{{#if ctrl.editing}}
{{d-button action="confirmEdit"
label="queue.confirm"
disabled=ctrl.post.isSaving
class="btn-primary confirm"}}
{{d-button action="cancelEdit"
label="queue.cancel"
icon="times"
disabled=ctrl.post.isSaving
class="btn-danger cancel"}}
{{else}}
{{d-button action="approve"
disabled=ctrl.post.isSaving
label="queue.approve"
icon="check"
class="btn-primary approve"}}
{{d-button action="reject"
disabled=ctrl.post.isSaving
label="queue.reject"
icon="times"
class="btn-danger reject"}}
{{d-button action="edit"
disabled=ctrl.post.isSaving
label="queue.edit"
icon="pencil"
class="edit"}}
{{/if}}
</div>
</div>
<div class='clearfix'></div>
</div>
{{else}}
<p>{{i18n "queue.none"}}</p>
{{/each}}
</div>
</div>

View File

@ -2,19 +2,23 @@
<ul class="location-links">
{{#if showAdminLinks}}
<li>
<a href="/admin" class="admin-link"><i class='fa fa-wrench'></i> {{i18n 'admin_title'}}</a>
{{#link-to "admin" class="admin-link"}}
<i class='fa fa-wrench'></i> {{i18n 'admin_title'}}
{{/link-to}}
</li>
<li>
<a href="/admin/flags/active" class="flagged-posts-link">
{{#link-to "adminFlags" class="flagged-posts-link"}}
{{fa-icon "flag"}} {{i18n 'flags_title'}}
{{#if currentUser.site_flagged_posts_count}}
<span title='{{i18n 'notifications.total_flagged'}}' class='badge-notification flagged-posts'>{{currentUser.site_flagged_posts_count}}</span>
{{/if}}
</a>
{{/link-to}}
</li>
{{/if}}
<li>
<a href="/latest" title="{{i18n 'filters.latest.help'}}" class="latest-topics-link">{{i18n 'filters.latest.title'}}</a>
{{#link-to "discovery.latest" class="latest-topics-link"}}
{{i18n 'filters.latest.title'}}
{{/link-to}}
</li>
{{#if showBadgesLink}}
<li>
@ -26,6 +30,17 @@
<li>{{#link-to 'users'}}{{i18n "directory.title"}}{{/link-to}}</li>
{{/if}}
{{#if currentUser.show_queued_posts}}
<li>
{{#link-to 'queued-posts'}}
{{i18n "queue.title"}}
{{#if currentUser.post_queue_new_count}}
<span class='badge-notification flagged-posts'>{{currentUser.post_queue_new_count}}</span>
{{/if}}
{{/link-to}}
</li>
{{/if}}
{{plugin-outlet "site-map-links"}}
{{#if showKeyboardShortcuts}}

View File

@ -4,6 +4,8 @@
{{discourse-banner user=currentUser banner=site.banner overlay=view.hasScrolled hide=errorLoading}}
</div>
{{plugin-outlet "topic-above-post-stream"}}
{{#if postStream.loaded}}
{{#if postStream.firstPostPresent}}
<div id='topic-title'>
@ -22,8 +24,8 @@
{{plugin-outlet "edit-topic"}}
{{d-button action="finishedEditingTopic" class="btn-primary btn-small no-text" icon="check"}}
{{d-button action="cancelEditingTopic" class="btn-small no-text" icon="times"}}
{{d-button action="finishedEditingTopic" class="btn-primary btn-small no-text submit-edit" icon="check"}}
{{d-button action="cancelEditingTopic" class="btn-small no-text cancel-edit" icon="times"}}
{{else}}
<h1>
{{#unless is_warning}}
@ -32,7 +34,7 @@
{{#if details.loaded}}
{{topic-status topic=model}}
<a href='{{unbound url}}' {{action "jumpTop"}}>
<a href='{{unbound url}}' {{action "jumpTop"}} class='fancy-title'>
{{{fancy_title}}}
</a>
{{/if}}
@ -87,6 +89,8 @@
{{view 'topic-closing' topic=model}}
{{view 'topic-footer-buttons' topic=model}}
{{plugin-outlet "topic-above-suggested"}}
{{#if details.suggested_topics.length}}
<div id='suggested-topics'>
<h3>{{i18n 'suggested_topics.title'}}</h3>

View File

@ -197,16 +197,17 @@
<div class="control-group other">
<label class="control-label">{{i18n 'user.other_settings'}}</label>
<div class="controls controls-dropdown">
<label>{{i18n 'user.auto_track_topics'}}</label>
{{combo-box valueAttribute="value" content=autoTrackDurations value=auto_track_topics_after_msecs}}
</div>
<div class="controls controls-dropdown">
<label>{{i18n 'user.new_topic_duration.label'}}</label>
{{combo-box valueAttribute="value" content=considerNewTopicOptions value=new_topic_duration_minutes}}
</div>
<div class="controls controls-dropdown">
<label>{{i18n 'user.auto_track_topics'}}</label>
{{combo-box valueAttribute="value" content=autoTrackDurations value=auto_track_topics_after_msecs}}
</div>
{{preference-checkbox labelKey="user.external_links_in_new_tab" checked=external_links_in_new_tab}}
{{preference-checkbox labelKey="user.enable_quoting" checked=enable_quoting}}
{{preference-checkbox labelKey="user.dynamic_favicon" checked=dynamic_favicon}}

View File

@ -168,7 +168,7 @@
{{#if canSeeNotificationHistory}}
{{#link-to 'user.notifications' tagName="li"}}
{{#link-to 'user.notifications'}}
<i class='glyph fa fa-comment'></i>
{{fa-icon "comment" class="glyph"}}
{{i18n 'user.notifications'}}
<span class='count'>({{notification_count}})</span>
{{/link-to}}

View File

@ -1,6 +0,0 @@
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/archetype_options',
title: I18n.t('topic.options')
});

View File

@ -1,6 +0,0 @@
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/change_owner',
title: I18n.t('topic.change_owner.title')
});

View File

@ -1,11 +1,3 @@
/**
Renders a popup messages on the composer
@class ComposerMessagesView
@extends Discourse.View
@namespace Discourse
@module Discourse
**/
export default Ember.CollectionView.extend({
classNameBindings: [':composer-popup-container', 'hidden'],
content: Em.computed.alias('controller.content'),
@ -16,19 +8,18 @@ export default Ember.CollectionView.extend({
classNames: ['composer-popup', 'hidden'],
templateName: Em.computed.alias('content.templateName'),
init: function() {
_setup: function() {
this._super();
this.set('context', this.get('content'));
if (this.get('content.extraClass')) {
this.get('classNames').pushObject(this.get('content.extraClass'));
}
},
}.on('init'),
didInsertElement: function() {
_initCss: function() {
var composerHeight = $('#reply-control').height() || 0;
this.$().css('bottom', composerHeight + "px").show();
}
}.on('didInsertElement')
})
});

View File

@ -573,6 +573,10 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
reason = I18n.t('composer.error.post_missing');
} else if( missingChars > 0 ) {
reason = I18n.t('composer.error.post_length', {min: this.get('model.minimumPostLength')});
let tl = Discourse.User.currentProp("trust_level");
if (tl === 0 || tl === 1) {
reason += "<br/>" + I18n.t('composer.error.try_like');
}
}
if( reason ) {

View File

@ -1,4 +1,6 @@
export default Ember.ContainerView.extend(Discourse.Presence, {
import Presence from 'discourse/mixins/presence';
export default Ember.ContainerView.extend(Presence, {
attachViewWithArgs(viewArgs, viewClass) {
if (!viewClass) { viewClass = Ember.View.extend(); }

Some files were not shown because too many files have changed in this diff Show More