Big commit:

- Support for a popup that shows similar topics
- Cleaned up a lot of Javascript
- Cleaned up use of Promises
This commit is contained in:
Robin Ward 2013-03-14 14:45:29 -04:00
parent 7714e2050e
commit ad082cea70
39 changed files with 584 additions and 560 deletions

View file

@ -456,7 +456,7 @@ GEM
turbo-sprockets-rails3 (0.3.6)
railties (> 3.2.8, < 4.0.0)
sprockets (>= 2.0.0)
tzinfo (0.3.35)
tzinfo (0.3.37)
uglifier (1.3.0)
execjs (>= 0.3.0)
multi_json (~> 1.0, >= 1.0.2)

View file

@ -156,15 +156,9 @@ Discourse.AdminUser.reopenClass({
},
find: function(username) {
var promise;
promise = new RSVP.Promise();
$.ajax({
url: "/admin/users/" + username,
success: function(result) {
return promise.resolve(Discourse.AdminUser.create(result));
}
});
return promise;
return $.ajax({url: "/admin/users/" + username}).then(function (result) {
return Discourse.AdminUser.create(result);
})
},
findAll: function(query, filter) {

View file

@ -46,47 +46,15 @@ Discourse.FlaggedPost = Discourse.Post.extend({
}).property('topic_hidden'),
deletePost: function() {
var promise;
promise = new RSVP.Promise();
if (this.get('post_number') === "1") {
return $.ajax("/t/" + this.topic_id, {
type: 'DELETE',
cache: false,
success: function() {
promise.resolve();
},
error: function(e) {
promise.reject();
}
});
return $.ajax("/t/" + this.topic_id, { type: 'DELETE', cache: false });
} else {
return $.ajax("/posts/" + this.id, {
type: 'DELETE',
cache: false,
success: function() {
promise.resolve();
},
error: function(e) {
promise.reject();
}
});
return $.ajax("/posts/" + this.id, { type: 'DELETE', cache: false });
}
},
clearFlags: function() {
var promise;
promise = new RSVP.Promise();
$.ajax("/admin/flags/clear/" + this.id, {
type: 'POST',
cache: false,
success: function() {
promise.resolve();
},
error: function(e) {
promise.reject();
}
});
return promise;
return $.ajax("/admin/flags/clear/" + this.id, { type: 'POST', cache: false });
},
hiddenClass: (function() {

View file

@ -26,14 +26,8 @@ Discourse.VersionCheck = Discourse.Model.extend({
Discourse.VersionCheck.reopenClass({
find: function() {
var promise = new RSVP.Promise();
$.ajax({
url: '/admin/version_check',
dataType: 'json',
success: function(json) {
promise.resolve(Discourse.VersionCheck.create(json));
}
});
return promise;
return $.ajax({ url: '/admin/version_check', dataType: 'json' }).then(function(json) {
return Discourse.VersionCheck.create(json);
});
}
});

View file

@ -8,22 +8,20 @@
**/
Discourse.ComposerController = Discourse.Controller.extend({
needs: ['modal', 'topic'],
hasReply: false,
togglePreview: function() {
return this.get('content').togglePreview();
this.get('content').togglePreview();
},
// Import a quote from the post
importQuote: function() {
return this.get('content').importQuote();
this.get('content').importQuote();
},
appendText: function(text) {
var c;
c = this.get('content');
var c = this.get('content');
if (c) {
return c.appendText(text);
c.appendText(text);
}
},
@ -72,7 +70,6 @@ Discourse.ComposerController = Discourse.Controller.extend({
}
bootbox.dialog(message, buttons);
return;
}
}
@ -94,17 +91,80 @@ Discourse.ComposerController = Discourse.Controller.extend({
});
},
checkReplyLength: function() {
if (this.present('content.reply')) {
this.set('hasReply', true);
} else {
this.set('hasReply', false);
closeEducation: function() {
this.set('educationClosed', true);
},
closeSimilar: function() {
this.set('similarClosed', true);
},
similarVisible: function() {
if (this.get('similarClosed')) return false;
if (this.get('content.composeState') !== Discourse.Composer.OPEN) return false;
return (this.get('similarTopics.length') || 0) > 0;
}.property('similarTopics.length', 'similarClosed', 'content.composeState'),
newUserEducationVisible: function() {
if (!this.get('educationContents')) return false;
if (this.get('content.composeState') !== Discourse.Composer.OPEN) return false;
if (!this.present('content.reply')) return false;
if (this.get('educationClosed')) return false;
return true;
}.property('content.composeState', 'content.reply', 'educationClosed', 'educationContents'),
fetchNewUserEducation: function() {
// If creating a topic, use topic_count, otherwise post_count
var count = this.get('content.creatingTopic') ? Discourse.get('currentUser.topic_count') : Discourse.get('currentUser.reply_count');
if (count >= Discourse.SiteSettings.educate_until_posts) {
this.set('educationClosed', true);
this.set('educationContents', '');
return;
}
// The user must have typed a reply
if (!this.get('typedReply')) return;
this.set('educationClosed', false);
// If visible update the text
var educationKey = this.get('content.creatingTopic') ? 'new-topic' : 'new-reply';
var composerController = this;
$.get("/education/" + educationKey).then(function(result) {
composerController.set('educationContents', result);
});
}.observes('typedReply', 'content.creatingTopic', 'Discourse.currentUser.reply_count'),
checkReplyLength: function() {
this.set('typedReply', this.present('content.reply'));
},
/**
Fired after a user stops typing. Considers whether to check for similar
topics based on the current composer state.
@method findSimilarTopics
**/
findSimilarTopics: function() {
// We don't care about similar topics unless creating a topic
if (!this.get('content.creatingTopic')) return;
var body = this.get('content.reply');
var title = this.get('content.title');
// Ensure the fields are of the minimum length
if (body.length < Discourse.SiteSettings.min_body_similar_length) return;
if (title.length < Discourse.SiteSettings.min_title_similar_length) return;
var composerController = this;
Discourse.Topic.findSimilarTo(title, body).then(function (topics) {
composerController.set('similarTopics', topics);
});
},
saveDraft: function() {
var model;
model = this.get('content');
var model = this.get('content');
if (model) model.saveDraft();
},
@ -123,8 +183,11 @@ Discourse.ComposerController = Discourse.Controller.extend({
_this = this;
if (!opts) opts = {};
opts.promise = promise = opts.promise || new RSVP.Promise();
this.set('hasReply', false);
opts.promise = promise = opts.promise || Ember.Deferred.create();
this.set('typedReply', false);
this.set('similarTopics', null);
this.set('similarClosed', false);
if (!opts.draftKey) {
alert("composer was opened without a draft key");
throw "composer opened without a proper draft key";
@ -133,9 +196,7 @@ Discourse.ComposerController = Discourse.Controller.extend({
// ensure we have a view now, without it transitions are going to be messed
view = this.get('view');
if (!view) {
view = Discourse.ComposerView.create({
controller: this
});
view = Discourse.ComposerView.create({ controller: this });
view.appendTo($('#main'));
this.set('view', view);
// the next runloop is too soon, need to get the control rendered and then
@ -197,8 +258,7 @@ Discourse.ComposerController = Discourse.Controller.extend({
},
wouldLoseChanges: function() {
var composer;
composer = this.get('content');
var composer = this.get('content');
return composer && composer.wouldLoseChanges();
},
@ -210,10 +270,9 @@ Discourse.ComposerController = Discourse.Controller.extend({
},
destroyDraft: function() {
var key;
key = this.get('content.draftKey');
var key = this.get('content.draftKey');
if (key) {
return Discourse.Draft.clear(key, this.get('content.draftSequence'));
Discourse.Draft.clear(key, this.get('content.draftSequence'));
}
},
@ -243,17 +302,17 @@ Discourse.ComposerController = Discourse.Controller.extend({
}
},
click: function() {
openIfDraft: function() {
if (this.get('content.composeState') === Discourse.Composer.DRAFT) {
return this.set('content.composeState', Discourse.Composer.OPEN);
this.set('content.composeState', Discourse.Composer.OPEN);
}
},
shrink: function() {
if (this.get('content.reply') === this.get('content.originalText')) {
return this.close();
this.close();
} else {
return this.collapse();
this.collapse();
}
},

View file

@ -42,13 +42,11 @@ Discourse.ListController = Discourse.Controller.extend({
this.set('loading', true);
if (filterMode === 'categories') {
return Ember.Deferred.promise(function(deferred) {
Discourse.CategoryList.list(filterMode).then(function(items) {
return Discourse.CategoryList.list(filterMode).then(function(items) {
listController.set('loading', false);
listController.set('filterMode', filterMode);
listController.set('categoryMode', true);
return deferred.resolve(items);
});
return items;
});
}
@ -56,13 +54,11 @@ Discourse.ListController = Discourse.Controller.extend({
if (!current) {
current = Discourse.NavItem.create({ name: filterMode });
}
return Ember.Deferred.promise(function(deferred) {
Discourse.TopicList.list(current).then(function(items) {
return Discourse.TopicList.list(current).then(function(items) {
listController.set('filterSummary', items.filter_summary);
listController.set('filterMode', filterMode);
listController.set('loading', false);
return deferred.resolve(items);
});
return items;
});
},

View file

@ -117,19 +117,18 @@ Discourse.TopicController = Discourse.ObjectController.extend({
},
replyAsNewTopic: function(post) {
var composerController, postLink, postUrl, promise;
composerController = this.get('controllers.composer');
// TODO shut down topic draft cleanly if it exists ...
promise = composerController.open({
var composerController = this.get('controllers.composer');
var promise = composerController.open({
action: Discourse.Composer.CREATE_TOPIC,
draftKey: Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY
});
postUrl = "" + location.protocol + "//" + location.host + (post.get('url'));
postLink = "[" + (this.get('title')) + "](" + postUrl + ")";
return promise.then(function() {
return Discourse.Post.loadQuote(post.get('id')).then(function(q) {
return composerController.appendText("" + (Em.String.i18n("post.continue_discussion", {
var postUrl = "" + location.protocol + "//" + location.host + (post.get('url'));
var postLink = "[" + (this.get('title')) + "](" + postUrl + ")";
promise.then(function() {
Discourse.Post.loadQuote(post.get('id')).then(function(q) {
composerController.appendText("" + (Em.String.i18n("post.continue_discussion", {
postLink: postLink
})) + "\n\n" + q);
});

View file

@ -40,8 +40,6 @@ Discourse.ActionSummary = Discourse.Model.extend({
act: function(opts) {
// Mark it as acted
var promise,
_this = this;
this.set('acted', true);
this.set('count', this.get('count') + 1);
this.set('can_act', false);
@ -53,26 +51,19 @@ Discourse.ActionSummary = Discourse.Model.extend({
}
// Create our post action
promise = new RSVP.Promise();
$.ajax({
var actionSummary = this;
return $.ajax({
url: "/post_actions",
type: 'POST',
data: {
id: this.get('post.id'),
post_action_type_id: this.get('id'),
message: (opts ? opts.message : void 0) || ""
},
error: function(error) {
var errors;
_this.removeAction();
errors = $.parseJSON(error.responseText).errors;
return promise.reject(errors);
},
success: function() {
return promise.resolve();
}
}).then(null, function (error) {
actionSummary.removeAction();
return $.parseJSON(error.responseText).errors;
});
return promise;
},
// Undo this action

View file

@ -31,18 +31,14 @@ Discourse.CategoryList.reopenClass({
},
list: function(filter) {
var promise,
_this = this;
promise = new RSVP.Promise();
$.getJSON("/" + filter + ".json").then(function(result) {
var categoryList;
categoryList = Discourse.TopicList.create();
var route = this;
return $.getJSON("/" + filter + ".json").then(function(result) {
var categoryList = Discourse.TopicList.create();
categoryList.set('can_create_category', result.category_list.can_create_category);
categoryList.set('categories', _this.categoriesFrom(result));
categoryList.set('categories', route.categoriesFrom(result));
categoryList.set('loaded', true);
return promise.resolve(categoryList);
return categoryList;
});
return promise;
}
});

View file

@ -85,9 +85,9 @@ Discourse.Composer = Discourse.Model.extend({
if (post) {
this.set('loading', true);
var composer = this;
return Discourse.Post.load(post.get('id'), function(result) {
Discourse.Post.load(post.get('id')).then(function(result) {
composer.appendText(Discourse.BBCode.buildQuoteBBCode(post, result.get('raw')));
return composer.set('loading', false);
composer.set('loading', false);
});
}
},
@ -249,9 +249,10 @@ Discourse.Composer = Discourse.Model.extend({
this.set('reply', opts.reply || this.get("reply") || "");
if (opts.postId) {
this.set('loading', true);
Discourse.Post.load(opts.postId, function(result) {
Discourse.Post.load(opts.postId).then(function(result) {
console.log(result);
composer.set('post', result);
return composer.set('loading', false);
composer.set('loading', false);
});
}
@ -259,7 +260,7 @@ Discourse.Composer = Discourse.Model.extend({
if (opts.action === EDIT && opts.post) {
this.set('title', this.get('topic.title'));
this.set('loading', true);
Discourse.Post.load(opts.post.get('id'), function(result) {
Discourse.Post.load(opts.post.get('id')).then(function(result) {
composer.set('reply', result.get('raw'));
composer.set('originalText', composer.get('reply'));
composer.set('loading', false);
@ -285,7 +286,6 @@ Discourse.Composer = Discourse.Model.extend({
// When you edit a post
editPost: function(opts) {
var promise = new RSVP.Promise();
var post = this.get('post');
var oldCooked = post.get('cooked');
var composer = this;
@ -304,15 +304,13 @@ Discourse.Composer = Discourse.Model.extend({
post.set('cooked', $('#wmd-preview').html());
this.set('composeState', CLOSED);
return Ember.Deferred.promise(function(promise) {
post.save(function(savedPost) {
var idx, postNumber;
var posts = composer.get('topic.posts');
// perhaps our post came from elsewhere eg. draft
idx = -1;
postNumber = post.get('post_number');
var idx = -1;
var postNumber = post.get('post_number');
posts.each(function(p, i) {
if (p.get('post_number') === postNumber) {
idx = i;
@ -331,13 +329,12 @@ Discourse.Composer = Discourse.Model.extend({
post.set('cooked', oldCooked);
return composer.set('composeState', OPEN);
});
return promise;
});
},
// Create a new Post
createPost: function(opts) {
var promise = new RSVP.Promise(),
post = this.get('post'),
var post = this.get('post'),
topic = this.get('topic'),
currentUser = Discourse.get('currentUser'),
addedToStream = false;
@ -401,6 +398,7 @@ Discourse.Composer = Discourse.Model.extend({
// Save callback
var composer = this;
return Ember.Deferred.promise(function(promise) {
createdPost.save(function(result) {
var addedPost = false,
saving = true;
@ -424,15 +422,13 @@ Discourse.Composer = Discourse.Model.extend({
return promise.resolve({ post: result });
}, function(error) {
// If an error occurs
var errors;
if (topic) {
topic.posts.removeObject(createdPost);
}
errors = $.parseJSON(error.responseText).errors;
promise.reject(errors[0]);
promise.reject($.parseJSON(error.responseText).errors[0]);
composer.set('composeState', OPEN);
});
return promise;
});
},
saveDraft: function() {

View file

@ -22,20 +22,11 @@ Discourse.Draft.reopenClass({
},
get: function(key) {
var promise,
_this = this;
promise = new RSVP.Promise();
$.ajax({
return $.ajax({
url: '/draft',
data: {
draft_key: key
},
dataType: 'json',
success: function(data) {
return promise.resolve(data);
}
data: { draft_key: key },
dataType: 'json'
});
return promise;
},
getLocal: function(key, current) {
@ -44,35 +35,16 @@ Discourse.Draft.reopenClass({
},
save: function(key, sequence, data) {
var promise;
promise = new RSVP.Promise();
data = typeof data === "string" ? data : JSON.stringify(data);
$.ajax({
return $.ajax({
type: 'POST',
url: "/draft",
data: {
draft_key: key,
data: data,
sequence: sequence
},
success: function() {
/* don't keep local
*/
/* Discourse.KeyValueStore.remove("draft_#{key}")
*/
return promise.resolve();
},
error: function() {
/* save local
*/
/* Discourse.KeyValueStore.set(key: "draft_#{key}", value: data)
*/
return promise.reject();
}
});
return promise;
}
});

View file

@ -15,13 +15,8 @@ Discourse.InviteList = Discourse.Model.extend({
Discourse.InviteList.reopenClass({
findInvitedBy: function(user) {
var promise;
promise = new RSVP.Promise();
$.ajax({
url: "/users/" + (user.get('username_lower')) + "/invited.json",
success: function(result) {
var invitedList;
invitedList = result.invited_list;
return $.ajax({ url: "/users/" + (user.get('username_lower')) + "/invited.json" }).then(function (result) {
var invitedList = result.invited_list;
if (invitedList.pending) {
invitedList.pending = invitedList.pending.map(function(i) {
return Discourse.Invite.create(i);
@ -33,10 +28,8 @@ Discourse.InviteList.reopenClass({
});
}
invitedList.user = user;
return promise.resolve(Discourse.InviteList.create(invitedList));
}
return Discourse.InviteList.create(invitedList);
});
return promise;
}
});

View file

@ -215,22 +215,19 @@ Discourse.Post = Discourse.Model.extend({
// Load replies to this post
loadReplies: function() {
var promise,
_this = this;
promise = new RSVP.Promise();
this.set('loadingReplies', true);
this.set('replies', []);
$.getJSON("/posts/" + (this.get('id')) + "/replies", function(loaded) {
var parent = this;
return $.ajax({url: "/posts/" + (this.get('id')) + "/replies"}).then(function(loaded) {
var replies = parent.get('replies');
loaded.each(function(reply) {
var post;
post = Discourse.Post.create(reply);
post.set('topic', _this.get('topic'));
return _this.get('replies').pushObject(post);
var post = Discourse.Post.create(reply);
post.set('topic', parent.get('topic'));
replies.pushObject(post);
});
_this.set('loadingReplies', false);
return promise.resolve();
parent.set('loadingReplies', false);
});
return promise;
},
loadVersions: function(callback) {
@ -293,43 +290,33 @@ Discourse.Post.reopenClass({
return $.ajax("/posts/destroy_many", {
type: 'DELETE',
data: {
post_ids: posts.map(function(p) {
return p.get('id');
})
post_ids: posts.map(function(p) { return p.get('id'); })
}
});
},
loadVersion: function(postId, version, callback) {
var _this = this;
return $.getJSON("/posts/" + postId + ".json?version=" + version, function(result) {
return callback(Discourse.Post.create(result));
return $.ajax({url: "/posts/" + postId + ".json?version=" + version}).then(function(result) {
return Discourse.Post.create(result);
});
},
loadByPostNumber: function(topicId, postId, callback) {
var _this = this;
return $.getJSON("/posts/by_number/" + topicId + "/" + postId + ".json", function(result) {
return callback(Discourse.Post.create(result));
loadByPostNumber: function(topicId, postId) {
return $.ajax({url: "/posts/by_number/" + topicId + "/" + postId + ".json"}).then(function (result) {
return Discourse.Post.create(result);
});
},
loadQuote: function(postId) {
var promise,
_this = this;
promise = new RSVP.Promise();
$.getJSON("/posts/" + postId + ".json", function(result) {
var post;
post = Discourse.Post.create(result);
return promise.resolve(Discourse.BBCode.buildQuoteBBCode(post, post.get('raw')));
return $.ajax({url: "/posts/" + postId + ".json"}).then(function(result) {
var post = Discourse.Post.create(result);
return Discourse.BBCode.buildQuoteBBCode(post, post.get('raw'));
});
return promise;
},
load: function(postId, callback) {
var _this = this;
return $.getJSON("/posts/" + postId + ".json", function(result) {
return callback(Discourse.Post.create(result));
load: function(postId) {
return $.ajax({url: "/posts/" + postId + ".json"}).then(function (result) {
return Discourse.Post.create(result);
});
}

View file

@ -372,6 +372,20 @@ Discourse.Topic.reopenClass({
MUTE: 0
},
/**
Find similar topics to a given title and body
@method findSimilar
@param {String} title The current title
@param {String} body The current body
@returns A promise that will resolve to the topics
**/
findSimilarTo: function(title, body) {
return $.ajax({url: "/topics/similar_to", data: {title: title, raw: body} }).then(function (results) {
return results.map(function(topic) { return Discourse.Topic.create(topic) });
});
},
// Load a topic, but accepts a set of filters
// options:
// onLoad - the callback after the topic is loaded
@ -408,20 +422,15 @@ Discourse.Topic.reopenClass({
}
// Check the preload store. If not, load it via JSON
promise = new RSVP.Promise();
PreloadStore.get("topic_" + topicId, function() {
return PreloadStore.get("topic_" + topicId, function() {
return $.getJSON(url + ".json", data);
}).then(function(result) {
var first;
first = result.posts.first();
var first = result.posts.first();
if (first && opts && opts.bestOf) {
first.bestOfFirst = true;
}
return promise.resolve(result);
}, function(result) {
return promise.reject(result);
return result;
});
return promise;
},
// Create a topic from posts

View file

@ -10,13 +10,11 @@
Discourse.TopicList = Discourse.Model.extend({
loadMoreTopics: function() {
var moreUrl, promise,
_this = this;
promise = new RSVP.Promise();
var moreUrl, _this = this;
if (moreUrl = this.get('more_topics_url')) {
Discourse.URL.replaceState("/" + (this.get('filter')) + "/more");
$.ajax(moreUrl, {
success: function(result) {
return $.ajax({url: moreUrl}).then(function (result) {
var newTopics, topicIds, topics, topicsAdded = 0;
if (result) {
// the new topics loaded from the server
@ -39,13 +37,11 @@ Discourse.TopicList = Discourse.Model.extend({
_this.set('more_topics_url', result.topic_list.more_topics_url);
Discourse.set('transient.topicsList', _this);
}
return promise.resolve(result.topic_list.more_topics_url ? true : false);
}
return result.topic_list.more_topics_url;
});
} else {
promise.resolve(false);
return null;
}
return promise;
},
insert: function(json) {
@ -90,7 +86,7 @@ Discourse.TopicList.reopenClass({
},
list: function(menuItem) {
var filter, found, list, promise, topic_list, url;
var filter, list, promise, topic_list, url;
filter = menuItem.name;
topic_list = Discourse.TopicList.create();
topic_list.set('inserted', Em.A());
@ -101,19 +97,16 @@ Discourse.TopicList.reopenClass({
}
if (list = Discourse.get('transient.topicsList')) {
if ((list.get('filter') === filter) && window.location.pathname.indexOf('more') > 0) {
promise = new RSVP.Promise();
list.set('loaded', true);
return Ember.Deferred.promise(function(promise) {
promise.resolve(list);
return promise;
});
}
}
Discourse.set('transient.topicsList', null);
Discourse.set('transient.topicListScrollPos', null);
promise = new RSVP.Promise();
found = PreloadStore.contains('topic_list');
PreloadStore.get("topic_list", function() {
return $.getJSON(url);
}).then(function(result) {
return PreloadStore.get("topic_list", function() { return $.getJSON(url) }).then(function(result) {
topic_list.set('topics', Discourse.TopicList.topicsFrom(result));
topic_list.set('can_create_topic', result.topic_list.can_create_topic);
topic_list.set('more_topics_url', result.topic_list.more_topics_url);
@ -125,9 +118,8 @@ Discourse.TopicList.reopenClass({
topic_list.set('category', Discourse.Category.create(result.topic_list.filtered_category));
}
topic_list.set('loaded', true);
return promise.resolve(topic_list);
return topic_list;
});
return promise;
}
});

View file

@ -76,7 +76,7 @@ Discourse.User = Discourse.Model.extend({
@returns Result of ajax call
**/
changeUsername: function(newUsername) {
return jQuery.ajax({
return $.ajax({
url: "/users/" + (this.get('username_lower')) + "/preferences/username",
type: 'PUT',
data: {
@ -93,7 +93,7 @@ Discourse.User = Discourse.Model.extend({
@returns Result of ajax call
**/
changeEmail: function(email) {
return jQuery.ajax({
return $.ajax({
url: "/users/" + (this.get('username_lower')) + "/preferences/email",
type: 'PUT',
data: {
@ -121,7 +121,7 @@ Discourse.User = Discourse.Model.extend({
**/
save: function(finished) {
var _this = this;
jQuery.ajax("/users/" + this.get('username').toLowerCase(), {
$.ajax("/users/" + this.get('username').toLowerCase(), {
data: this.getProperties('auto_track_topics_after_msecs',
'bio_raw',
'website',
@ -153,7 +153,7 @@ Discourse.User = Discourse.Model.extend({
changePassword: function(callback) {
var good;
good = false;
jQuery.ajax({
$.ajax({
url: '/session/forgot_password',
dataType: 'json',
data: {
@ -199,7 +199,7 @@ Discourse.User = Discourse.Model.extend({
var stream,
_this = this;
stream = this.get('stream');
jQuery.ajax({
$.ajax({
url: "/user_actions/" + id + ".json",
dataType: 'json',
cache: 'false',
@ -241,7 +241,7 @@ Discourse.User = Discourse.Model.extend({
url += "&filter=" + (this.get('streamFilter'));
}
return jQuery.ajax({
return $.ajax({
url: url,
dataType: 'json',
cache: 'false',
@ -362,7 +362,7 @@ Discourse.User.reopenClass({
@param {String} email An email address to check
**/
checkUsername: function(username, email) {
return jQuery.ajax({
return $.ajax({
url: '/users/check_username',
type: 'GET',
data: {
@ -465,7 +465,7 @@ Discourse.User.reopenClass({
@returns Result of ajax call
**/
createAccount: function(name, email, password, username, passwordConfirm, challenge) {
return jQuery.ajax({
return $.ajax({
url: '/users',
dataType: 'json',
data: {

View file

@ -2,10 +2,20 @@
<div class='contents'>
<div id='new-user-education' style='display: none'>
<a href='#' {{action closeEducation target="view"}} class='close'>{{i18n ok}}</a>
<div id='new-user-education' class='composer-popup' style='display: none'>
<a href='#' {{action closeEducation}} class='close'>{{i18n ok}}</a>
{{{controller.educationContents}}}
</div>
{{{view.educationContents}}}
<div id='similar-topics' class='composer-popup' style='display: none'>
<a href='#' {{action closeSimilar}} class='close'>{{i18n ok}}</a>
<h3>{{i18n composer.similar_topics}}<h3>
<ul class='topics'>
{{#each controller.similarTopics}}
<li>{{{topicLink this}}}</li>
{{/each}}
</ul>
</div>
<div class='control'>

View file

@ -19,36 +19,32 @@ Discourse.ComposerView = Discourse.View.extend({
'content.creatingTopic:topic',
'content.showPreview',
'content.hidePreview'],
educationClosed: null,
composeState: (function() {
var state;
state = this.get('content.composeState');
if (!state) {
state = Discourse.Composer.CLOSED;
}
return state;
}).property('content.composeState'),
composeState: function() {
var state = this.get('content.composeState');
if (state) return state;
return Discourse.Composer.CLOSED;
}.property('content.composeState'),
draftStatus: (function() {
return this.$('.saving-draft').text(this.get('content.draftStatus') || "");
}).observes('content.draftStatus'),
draftStatus: function() {
this.$('.saving-draft').text(this.get('content.draftStatus') || "");
}.observes('content.draftStatus'),
// Disable fields when we're loading
loadingChanged: (function() {
loadingChanged: function() {
if (this.get('loading')) {
$('#wmd-input, #reply-title').prop('disabled', 'disabled');
} else {
$('#wmd-input, #reply-title').prop('disabled', '');
}
}).observes('loading'),
}.observes('loading'),
postMade: (function() {
postMade: function() {
if (this.present('controller.createdPost')) return 'created-post';
return null;
}).property('content.createdPost'),
}.property('content.createdPost'),
observeReplyChanges: (function() {
observeReplyChanges: function() {
var _this = this;
if (this.get('content.hidePreview')) return;
Ember.run.next(null, function() {
@ -65,89 +61,70 @@ Discourse.ComposerView = Discourse.View.extend({
}
}
});
}).observes('content.reply', 'content.hidePreview'),
}.observes('content.reply', 'content.hidePreview'),
closeEducation: function() {
this.set('educationClosed', true);
return false;
},
fetchNewUserEducation: (function() {
// If creating a topic, use topic_count, otherwise post_count
var count, educationKey,
_this = this;
count = this.get('content.creatingTopic') ? Discourse.get('currentUser.topic_count') : Discourse.get('currentUser.reply_count');
if (count >= Discourse.SiteSettings.educate_until_posts) {
this.set('educationClosed', true);
this.set('educationContents', '');
return;
}
if (!this.get('controller.hasReply')) {
return;
}
this.set('educationClosed', false);
// If visible update the text
educationKey = this.get('content.creatingTopic') ? 'new-topic' : 'new-reply';
return $.get("/education/" + educationKey).then(function(result) {
return _this.set('educationContents', result);
});
}).observes('controller.hasReply', 'content.creatingTopic', 'Discourse.currentUser.reply_count'),
newUserEducationVisible: (function() {
if (!this.get('educationContents')) return false;
if (this.get('content.composeState') !== Discourse.Composer.OPEN) return false;
if (!this.present('content.reply')) return false;
if (this.get('educationClosed')) return false;
return true;
}).property('content.composeState', 'content.reply', 'educationClosed', 'educationContents'),
newUserEducationVisibilityChanged: (function() {
var $panel;
$panel = $('#new-user-education');
if (this.get('newUserEducationVisible')) {
return $panel.slideDown('fast');
newUserEducationVisibilityChanged: function() {
var $panel = $('#new-user-education');
if (this.get('controller.newUserEducationVisible')) {
$panel.slideDown('fast');
} else {
return $panel.slideUp('fast');
$panel.slideUp('fast')
}
}).observes('newUserEducationVisible'),
}.observes('controller.newUserEducationVisible'),
moveNewUserEducation: function(sizePx) {
$('#new-user-education').css('bottom', sizePx);
similarVisibilityChanged: function() {
var $panel = $('#similar-topics');
if (this.get('controller.similarVisible')) {
$panel.slideDown('fast');
} else {
$panel.slideUp('fast')
}
}.observes('controller.similarVisible'),
movePanels: function(sizePx) {
$('.composer-popup').css('bottom', sizePx);
},
resize: (function() {
resize: function() {
// this still needs to wait on animations, need a clean way to do that
var _this = this;
return Em.run.next(null, function() {
var h, replyControl, sizePx;
replyControl = $('#reply-control');
h = replyControl.height() || 0;
sizePx = "" + h + "px";
var replyControl = $('#reply-control');
var h = replyControl.height() || 0;
var sizePx = "" + h + "px";
$('.topic-area').css('padding-bottom', sizePx);
return $('#new-user-education').css('bottom', sizePx);
$('.composer-popup').css('bottom', sizePx);
});
}).observes('content.composeState'),
}.observes('content.composeState'),
keyUp: function(e) {
var controller;
controller = this.get('controller');
var controller = this.get('controller');
controller.checkReplyLength();
var lastKeyUp = new Date();
this.set('lastKeyUp', lastKeyUp);
// One second from now, check to see if the last key was hit when
// we recorded it. If it was, the user paused typing.
var composerView = this;
Em.run.later(function() {
if (lastKeyUp !== composerView.get('lastKeyUp')) return;
// Search for similar topics if the user pauses typing
controller.findSimilarTopics();
}, 1000);
// If the user hit ESC
if (e.which === 27) controller.hitEsc();
},
didInsertElement: function() {
var replyControl;
replyControl = $('#reply-control');
replyControl.DivResizer({
resize: this.resize,
onDrag: this.moveNewUserEducation
});
var replyControl = $('#reply-control');
replyControl.DivResizer({ resize: this.resize, onDrag: this.movePanels });
Discourse.TransitionHelper.after(replyControl, this.resize);
},
click: function() {
return this.get('controller').click();
this.get('controller').openIfDraft();
},
// Called after the preview renders. Debounced for performance
@ -229,7 +206,7 @@ Discourse.ComposerView = Discourse.View.extend({
});
},
onChangeItems: function(items) {
items = jQuery.map(items, function(i) {
items = $.map(items, function(i) {
if (i.username) {
return i.username;
} else {
@ -243,9 +220,7 @@ Discourse.ComposerView = Discourse.View.extend({
transformComplete: transformTemplate,
reverseTransform: function(i) {
return {
username: i
};
return { username: i };
}
});

View file

@ -58,21 +58,23 @@ Discourse.ListTopicsView = Discourse.View.extend(Discourse.Scrolling, {
},
loadMore: function() {
var _this = this;
if (this.get('loading')) {
return;
}
if (this.get('loading')) return;
this.set('loading', true);
return this.get('controller.content').loadMoreTopics().then(function(hasMoreResults) {
_this.set('loadedMore', true);
_this.set('loading', false);
Em.run.next(function() {
return _this.saveScrollPos();
});
var listTopicsView = this;
var promise = this.get('controller.content').loadMoreTopics();
if (promise) {
promise.then(function(hasMoreResults) {
listTopicsView.set('loadedMore', true);
listTopicsView.set('loading', false);
Em.run.next(function() { listTopicsView.saveScrollPos(); });
if (!hasMoreResults) {
return _this.get('eyeline').flushRest();
listTopicsView.get('eyeline').flushRest();
}
});
} else {
this.set('loading', false);
}
},
// Remember where we were scrolled to

View file

@ -12,28 +12,27 @@ Discourse.HistoryView = Discourse.View.extend({
modalClass: 'history-modal',
loadSide: function(side) {
var orig, version,
_this = this;
if (this.get("version" + side)) {
orig = this.get('originalPost');
version = this.get("version" + side + ".number");
var orig = this.get('originalPost');
var version = this.get("version" + side + ".number");
if (version === orig.get('version')) {
return this.set("post" + side, orig);
this.set("post" + side, orig);
} else {
return Discourse.Post.loadVersion(orig.get('id'), version, function(post) {
return _this.set("post" + side, post);
var historyView = this;
Discourse.Post.loadVersion(orig.get('id'), version).then(function(post) {
historyView.set("post" + side, post);
});
}
}
},
changedLeftVersion: (function() {
return this.loadSide("Left");
}).observes('versionLeft'),
changedLeftVersion: function() {
this.loadSide("Left");
}.observes('versionLeft'),
changedRightVersion: (function() {
return this.loadSide("Right");
}).observes('versionRight'),
changedRightVersion: function() {
this.loadSide("Right");
}.observes('versionRight'),
didInsertElement: function() {
var _this = this;

View file

@ -109,9 +109,8 @@ Discourse.PostView = Discourse.View.extend({
// Toggle visibility of parent post
toggleParent: function(e) {
var $parent, post,
_this = this;
$parent = this.$('.parent-post');
var postView = this;
var $parent = this.$('.parent-post');
if (this.get('parentPost')) {
$('nav', $parent).removeClass('toggled');
// Don't animate on touch
@ -119,19 +118,18 @@ Discourse.PostView = Discourse.View.extend({
$parent.hide();
this.set('parentPost', null);
} else {
$parent.slideUp(function() {
return _this.set('parentPost', null);
});
$parent.slideUp(function() { postView.set('parentPost', null); });
}
} else {
post = this.get('post');
var post = this.get('post');
this.set('loadingParent', true);
$('nav', $parent).addClass('toggled');
Discourse.Post.loadByPostNumber(post.get('topic_id'), post.get('reply_to_post_number'), function(result) {
_this.set('loadingParent', false);
Discourse.Post.loadByPostNumber(post.get('topic_id'), post.get('reply_to_post_number')).then(function(result) {
postView.set('loadingParent', false);
// Give the post a reference back to the topic
result.topic = _this.get('post.topic');
return _this.set('parentPost', result);
result.topic = postView.get('post.topic');
postView.set('parentPost', result);
});
}
return false;

View file

@ -235,7 +235,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
if (this.loading) return;
this.set('loading', true);
this.set('loadingAbove', true);
opts = jQuery.extend({
opts = $.extend({
postsBefore: post.get('post_number')
}, this.get('controller.postFilters'));
@ -303,9 +303,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
if (this.topic.posts.last().post_number !== post.post_number) return;
this.set('loadingBelow', true);
this.set('loading', true);
opts = jQuery.extend({
postsAfter: post.get('post_number')
}, this.get('controller.postFilters'));
opts = $.extend({ postsAfter: post.get('post_number') }, this.get('controller.postFilters'));
return Discourse.Topic.find(this.get('topic.id'), opts).then(function(result) {
var suggested;
if (result.at_bottom || result.posts.length === 0) {

View file

@ -28,14 +28,14 @@ PreloadStore = {
@method get
@param {String} key the key to look up the object with
@param {function} finder a function to find the object with
@returns {Promise} a promise that will eventually be the object we want.
@returns {Ember.Deferred} a promise that will eventually be the object we want.
**/
get: function(key, finder) {
var promise = new RSVP.Promise();
if (this.data[key]) {
promise.resolve(this.data[key]);
delete this.data[key];
var preloadStore = this;
return Ember.Deferred.promise(function(promise) {
if (preloadStore.data[key]) {
promise.resolve(preloadStore.data[key]);
delete preloadStore.data[key];
} else {
if (finder) {
@ -55,7 +55,7 @@ PreloadStore = {
promise.resolve(null);
}
}
return promise;
});
},
/**

View file

@ -4,7 +4,7 @@
@import "foundation/mixins";
#new-user-education {
.composer-popup {
@include box-shadow(3px 3px 3px rgba($black, 0.14));
@ -17,10 +17,21 @@
}
background-color: lighten($yellow, 40%);
border: 1px solid $yellow;
border: 1px solid darken($yellow, 5%);
padding: 10px;
width: 600px;
position: absolute;
ul.topics {
list-style: none;
margin: 0;
padding: 0;
li {
font-weight: normal;
margin-top: 3px;
}
}
}
#reply-control {

View file

@ -59,6 +59,17 @@ class TopicsController < ApplicationController
render nothing: true
end
def similar_to
requires_parameters(:title, :raw)
title, raw = params[:title], params[:raw]
raise Discourse::InvalidParameters.new(:title) if title.length < SiteSetting.min_title_similar_length
raise Discourse::InvalidParameters.new(:raw) if raw.length < SiteSetting.min_body_similar_length
topics = Topic.similar_to(title, raw)
render_serialized(topics, BasicTopicSerializer)
end
def status
requires_parameters(:status, :enabled)

View file

@ -34,6 +34,8 @@ class SiteSetting < ActiveRecord::Base
client_setting(:supress_reply_directly_below, true)
client_setting(:email_domains_blacklist, 'mailinator.com')
client_setting(:version_checks, true)
client_setting(:min_title_similar_length, 10)
client_setting(:min_body_similar_length, 15)
# settings only available server side
setting(:auto_track_topics_after, 300000)

View file

@ -213,8 +213,22 @@ class Topic < ActiveRecord::Base
id).to_a
end
def update_status(property, status, user)
# Search for similar topics
def self.similar_to(title, raw)
return [] unless title.present?
return [] unless raw.present?
# For now, we only match on title. We'll probably add body later on, hence the API hook
Topic.select(sanitize_sql_array(["topics.*, similarity(topics.title, :title) AS similarity", title: title]))
.visible
.where(closed: false, archived: false)
.listable_topics
.limit(5)
.order('similarity desc')
.all
end
def update_status(property, status, user)
Topic.transaction do
# Special case: if it's pinned, update that
@ -592,7 +606,6 @@ class Topic < ActiveRecord::Base
def clear_pin_for(user)
return unless user.present?
TopicUser.change(user.id, id, cleared_pinned_at: Time.now)
end

View file

@ -1,58 +1,9 @@
require_dependency 'age_words'
# The most basic attributes of a topic that we need to create a link for it.
class BasicTopicSerializer < ApplicationSerializer
include ActionView::Helpers
attributes :id,
:title,
:fancy_title,
:reply_count,
:posts_count,
:highest_post_number,
:image_url,
:created_at,
:last_posted_at,
:age,
:unseen,
:last_read_post_number,
:unread,
:new_posts
def age
AgeWords.age_words(Time.now - (object.created_at || Time.now))
end
def seen
object.user_data.present?
end
def unseen
return false if scope.blank?
return false if scope.user.blank?
return false if object.user_data.present?
return false if object.created_at < scope.user.treat_as_new_topic_start_date
true
end
def last_read_post_number
object.user_data.last_read_post_number
end
alias :include_last_read_post_number? :seen
def unread
unread_helper.unread_posts
end
alias :include_unread? :seen
def new_posts
unread_helper.new_posts
end
alias :include_new_posts? :seen
protected
def unread_helper
@unread_helper ||= Unread.new(object, object.user_data)
end
attributes :id, :fancy_title, :slug
end

View file

@ -1,10 +1,6 @@
class CategoryTopicSerializer < BasicTopicSerializer
attributes :slug,
:visible,
:closed,
:archived
class CategoryTopicSerializer < ListableTopicSerializer
attributes :visible, :closed, :archived
has_one :category
end

View file

@ -0,0 +1,56 @@
require_dependency 'age_words'
class ListableTopicSerializer < BasicTopicSerializer
include ActionView::Helpers
attributes :reply_count,
:posts_count,
:highest_post_number,
:image_url,
:created_at,
:last_posted_at,
:age,
:unseen,
:last_read_post_number,
:unread,
:new_posts,
:title
def age
AgeWords.age_words(Time.now - (object.created_at || Time.now))
end
def seen
object.user_data.present?
end
def unseen
return false if scope.blank?
return false if scope.user.blank?
return false if object.user_data.present?
return false if object.created_at < scope.user.treat_as_new_topic_start_date
true
end
def last_read_post_number
object.user_data.last_read_post_number
end
alias :include_last_read_post_number? :seen
def unread
unread_helper.unread_posts
end
alias :include_unread? :seen
def new_posts
unread_helper.new_posts
end
alias :include_new_posts? :seen
protected
def unread_helper
@unread_helper ||= Unread.new(object, object.user_data)
end
end

View file

@ -1,6 +1,6 @@
class SuggestedTopicSerializer < BasicTopicSerializer
class SuggestedTopicSerializer < ListableTopicSerializer
attributes :archetype, :slug, :like_count, :views, :last_post_age
attributes :archetype, :like_count, :views, :last_post_age
has_one :category, embed: :objects
def last_post_age

View file

@ -1,6 +1,6 @@
require_dependency 'pinned_check'
class TopicListItemSerializer < BasicTopicSerializer
class TopicListItemSerializer < ListableTopicSerializer
attributes :views,
:like_count,
@ -11,8 +11,7 @@ class TopicListItemSerializer < BasicTopicSerializer
:last_post_age,
:starred,
:has_best_of,
:archetype,
:slug
:archetype
has_one :category
has_many :posters, serializer: TopicPosterSerializer, embed: :objects

View file

@ -267,6 +267,7 @@ en:
saving_draft_tip: "saving"
saved_draft_tip: "saved"
saved_local_draft_tip: "saved locally"
similar_topics: "Similar Topics"
min_length:
at_least: "enter at least {{n}} characters"

View file

@ -408,6 +408,9 @@ en:
new_user_period_days: "How long a user is highlighted as being new, in days"
title_fancy_entities: "Convert fancy HTML entities in topic titles"
min_title_similar_length: "The minimum length of a title before it will be checked for similar topics"
min_body_similar_length: "The minimum length of a post's body before it will be checked for similar topics"
notification_types:
mentioned: "%{display_username} mentioned you in %{link}"
liked: "%{display_username} liked your post in %{link}"

View file

@ -164,7 +164,8 @@ Discourse::Application.routes.draw do
delete 't/:id' => 'topics#destroy'
put 't/:id' => 'topics#update'
post 't' => 'topics#create'
post 'topics/timings' => 'topics#timings'
post 'topics/timings'
get 'topics/similar_to'
# Legacy route for old avatars
get 'threads/:topic_id/:post_number/avatar' => 'topics#avatar', :constraints => {:topic_id => /\d+/, :post_number => /\d+/}

View file

@ -0,0 +1,9 @@
class EnableTrigramSupport < ActiveRecord::Migration
def up
execute "CREATE EXTENSION IF NOT EXISTS pg_trgm"
end
def down
execute "DROP EXTENSION pg_trgm"
end
end

View file

@ -70,10 +70,39 @@ describe TopicsController do
end
end
end
context 'similar_to' do
let(:title) { 'this title is long enough to search for' }
let(:raw) { 'this body is long enough to search for' }
it "requires a title" do
-> { xhr :get, :similar_to, raw: raw }.should raise_error(Discourse::InvalidParameters)
end
it "requires a raw body" do
-> { xhr :get, :similar_to, title: title }.should raise_error(Discourse::InvalidParameters)
end
it "raises an error if the title length is below the minimum" do
SiteSetting.stubs(:min_title_similar_length).returns(100)
-> { xhr :get, :similar_to, title: title, raw: raw }.should raise_error(Discourse::InvalidParameters)
end
it "raises an error if the body length is below the minimum" do
SiteSetting.stubs(:min_body_similar_length).returns(100)
-> { xhr :get, :similar_to, title: title, raw: raw }.should raise_error(Discourse::InvalidParameters)
end
it "delegates to Topic.similar_to" do
Topic.expects(:similar_to).with(title, raw).returns([Fabricate(:topic)])
xhr :get, :similar_to, title: title, raw: raw
end
end
context 'clear_pin' do
it 'needs you to be logged in' do
lambda { xhr :put, :clear_pin, topic_id: 1 }.should raise_error(Discourse::NotLoggedIn)

View file

@ -68,9 +68,7 @@ describe("PreloadStore", function() {
var done, finder, storeResult;
done = storeResult = null;
finder = function() {
var promise = new RSVP.Promise();
promise.resolve('evil');
return promise;
return Ember.Deferred.promise(function(promise) { promise.resolve('evil'); });
};
PreloadStore.get('joker', finder).then(function(result) {
done = true;
@ -86,9 +84,7 @@ describe("PreloadStore", function() {
var done, finder, storeResult;
done = storeResult = null;
finder = function() {
var promise = new RSVP.Promise();
promise.reject('evil');
return promise;
return Ember.Deferred.promise(function(promise) { promise.reject('evil'); });
};
PreloadStore.get('joker', finder).then(null, function(rejectedResult) {
done = true;

View file

@ -155,6 +155,24 @@ describe Topic do
end
context 'similar_to' do
it 'returns blank with nil params' do
Topic.similar_to(nil, nil).should be_blank
end
context 'with a similar topic' do
let!(:topic) { Fabricate(:topic, title: "Evil trout is the dude who posted this topic") }
it 'returns the similar topic if the title is similar' do
Topic.similar_to("has evil trout made any topics?", "i am wondering has evil trout made any topics?").should == [topic]
end
end
end
context 'message bus' do
it 'calls the message bus observer after create' do
MessageBusObserver.any_instance.expects(:after_create_topic).with(instance_of(Topic))