From 5eaae063f02c170adea99ab07d51883e853ed987 Mon Sep 17 00:00:00 2001 From: Robin Ward <robin.ward@gmail.com> Date: Thu, 11 Jul 2013 19:35:52 -0400 Subject: [PATCH] Discourse Macro Helpers + Minor Fix to Admin User View --- .../admin_users_list_controller.js | 4 +- .../javascripts/admin/models/admin_user.js | 13 ++--- .../admin/routes/admin_user_route.js | 6 +-- .../discourse/components/computed.js | 43 ++++++++++++++++ .../controllers/list_categories_controller.js | 3 +- .../discourse/controllers/topic_controller.js | 2 - .../javascripts/discourse/models/post.js | 18 ++----- .../javascripts/discourse/models/topic.js | 18 +++---- .../javascripts/discourse/models/user.js | 27 +++++----- .../templates/user/activity.js.handlebars | 2 +- app/controllers/posts_controller.rb | 18 +++---- test/javascripts/components/computed_test.js | 50 +++++++++++++++++++ test/javascripts/test_helper.js | 1 + 13 files changed, 136 insertions(+), 69 deletions(-) create mode 100644 test/javascripts/components/computed_test.js diff --git a/app/assets/javascripts/admin/controllers/admin_users_list_controller.js b/app/assets/javascripts/admin/controllers/admin_users_list_controller.js index 674eb5edf7d..6a95089a8d2 100644 --- a/app/assets/javascripts/admin/controllers/admin_users_list_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_users_list_controller.js @@ -78,9 +78,7 @@ Discourse.AdminUsersListController = Ember.ArrayController.extend(Discourse.Pres @property hasSelection **/ - hasSelection: function() { - return this.get('selectedCount') > 0; - }.property('selectedCount'), + hasSelection: Em.computed.gt('selectedCount', 0), /** Refresh the current list of users. diff --git a/app/assets/javascripts/admin/models/admin_user.js b/app/assets/javascripts/admin/models/admin_user.js index acdadfbb8c8..90b063e0166 100644 --- a/app/assets/javascripts/admin/models/admin_user.js +++ b/app/assets/javascripts/admin/models/admin_user.js @@ -98,19 +98,14 @@ Discourse.AdminUser = Discourse.User.extend({ this.set('trustLevel.id', this.get('originalTrustLevel')); }, - isBanned: (function() { - return this.get('is_banned') === true; - }).property('is_banned'), + isBanned: Em.computed.equal('is_banned', true), + canBan: Em.computed.not('staff'), - canBan: (function() { - return !this.get('admin') && !this.get('moderator'); - }).property('admin', 'moderator'), - - banDuration: (function() { + banDuration: function() { var banned_at = moment(this.banned_at); var banned_till = moment(this.banned_till); return banned_at.format('L') + " - " + banned_till.format('L'); - }).property('banned_till', 'banned_at'), + }.property('banned_till', 'banned_at'), ban: function() { var duration = parseInt(window.prompt(I18n.t('admin.user.ban_duration')), 10); diff --git a/app/assets/javascripts/admin/routes/admin_user_route.js b/app/assets/javascripts/admin/routes/admin_user_route.js index da0063e9999..92b6c8f1544 100644 --- a/app/assets/javascripts/admin/routes/admin_user_route.js +++ b/app/assets/javascripts/admin/routes/admin_user_route.js @@ -16,11 +16,6 @@ Discourse.AdminUserRoute = Discourse.Route.extend(Discourse.ModelReady, { return Discourse.AdminUser.find(Em.get(params, 'username').toLowerCase()); }, - setupController: function(controller, model) { - controller.set('model', model); - model.setOriginalTrustLevel(); - }, - renderTemplate: function() { this.render({into: 'admin/templates/admin'}); }, @@ -28,6 +23,7 @@ Discourse.AdminUserRoute = Discourse.Route.extend(Discourse.ModelReady, { modelReady: function(controller, adminUser) { adminUser.loadDetails(); controller.set('model', adminUser); + adminUser.setOriginalTrustLevel(); } }); diff --git a/app/assets/javascripts/discourse/components/computed.js b/app/assets/javascripts/discourse/components/computed.js index 33bc35d2b32..0ac946f49d8 100644 --- a/app/assets/javascripts/discourse/components/computed.js +++ b/app/assets/javascripts/discourse/components/computed.js @@ -12,6 +12,49 @@ Discourse.computed = { return Ember.computed(function() { return this.get(p1) === this.get(p2); }).property(p1, p2); + }, + + /** + Uses an Ember String `fmt` call to format a string. See: + http://emberjs.com/api/classes/Ember.String.html#method_fmt + + @method fmt + @params {String} properties* to format + @params {String} format the format string + @return {Function} computedProperty function + **/ + fmt: function() { + var args = Array.prototype.slice.call(arguments, 0); + var format = args.pop(); + var computed = Ember.computed(function() { + var context = this; + return format.fmt.apply(format, args.map(function (a) { + return context.get(a); + })); + }) + return computed.property.apply(computed, args); + }, + + /** + Creates a URL using Discourse.getURL. It takes a fmt string just like + fmt does. + + @method url + @params {String} properties* to format + @params {String} format the format string for the URL + @return {Function} computedProperty function returning a URL + **/ + url: function() { + var args = Array.prototype.slice.call(arguments, 0); + var format = args.pop(); + var computed = Ember.computed(function() { + var context = this; + return Discourse.getURL(format.fmt.apply(format, args.map(function (a) { + return context.get(a); + }))); + }) + return computed.property.apply(computed, args); + } }; diff --git a/app/assets/javascripts/discourse/controllers/list_categories_controller.js b/app/assets/javascripts/discourse/controllers/list_categories_controller.js index 9390d7ed552..42cd9a6e725 100644 --- a/app/assets/javascripts/discourse/controllers/list_categories_controller.js +++ b/app/assets/javascripts/discourse/controllers/list_categories_controller.js @@ -25,8 +25,7 @@ Discourse.ListCategoriesController = Discourse.ObjectController.extend({ }.property('categories.@each'), canEdit: function() { - var u = Discourse.User.current(); - return u && u.staff; + Discourse.User.current('staff'); }.property(), // clear a pinned topic diff --git a/app/assets/javascripts/discourse/controllers/topic_controller.js b/app/assets/javascripts/discourse/controllers/topic_controller.js index 42443d40c19..adb15c1a3ad 100644 --- a/app/assets/javascripts/discourse/controllers/topic_controller.js +++ b/app/assets/javascripts/discourse/controllers/topic_controller.js @@ -198,8 +198,6 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected Discourse.URL.routeTo(this.get('lastPostUrl')); }, - - replyAsNewTopic: function(post) { // TODO shut down topic draft cleanly if it exists ... var composerController = this.get('controllers.composer'); diff --git a/app/assets/javascripts/discourse/models/post.js b/app/assets/javascripts/discourse/models/post.js index 7ef7de00da6..5e43bbce072 100644 --- a/app/assets/javascripts/discourse/models/post.js +++ b/app/assets/javascripts/discourse/models/post.js @@ -21,13 +21,7 @@ Discourse.Post = Discourse.Model.extend({ return Discourse.Utilities.postUrl(this.get('topic.slug') || this.get('topic_slug'), this.get('topic_id'), this.get('post_number')); }.property('post_number', 'topic_id', 'topic.slug'), - originalPostUrl: function() { - return Discourse.getURL("/t/") + (this.get('topic_id')) + "/" + (this.get('reply_to_post_number')); - }.property('reply_to_post_number'), - - usernameUrl: function() { - return Discourse.getURL("/users/" + this.get('username')); - }.property('username'), + usernameUrl: Discourse.computed.url('username', '/users/%@'), showUserReplyTab: function() { return this.get('reply_to_user') && ( @@ -36,15 +30,9 @@ Discourse.Post = Discourse.Model.extend({ ); }.property('reply_to_user', 'reply_to_post_number', 'post_number'), - byTopicCreator: function() { - return this.get('topic.details.created_by.id') === this.get('user_id'); - }.property('topic.details.created_by.id', 'user_id'), - + byTopicCreator: Discourse.computed.propertyEqual('topic.details.created_by.id', 'user_id'), hasHistory: Em.computed.gt('version', 1), - - postElementId: function() { - return "post_" + (this.get('post_number')); - }.property('post_number'), + postElementId: Discourse.computed.fmt('post_number', 'post_%@'), // The class for the read icon of the post. It starts with read-icon then adds 'seen' or // 'last-read' if the post has been seen or is the highest post number seen so far respectively. diff --git a/app/assets/javascripts/discourse/models/topic.js b/app/assets/javascripts/discourse/models/topic.js index 8d6b084908a..66bde67445c 100644 --- a/app/assets/javascripts/discourse/models/topic.js +++ b/app/assets/javascripts/discourse/models/topic.js @@ -132,7 +132,6 @@ Discourse.Topic = Discourse.Model.extend({ archetypeObject: function() { return Discourse.Site.instance().get('archetypes').findProperty('id', this.get('archetype')); }.property('archetype'), - isPrivateMessage: Em.computed.equal('archetype', 'private_message'), toggleStatus: function(property) { @@ -225,7 +224,6 @@ Discourse.Topic = Discourse.Model.extend({ @method clearPin **/ clearPin: function() { - var topic = this; // Clear the pin optimistically from the object @@ -241,29 +239,27 @@ Discourse.Topic = Discourse.Model.extend({ // Is the reply to a post directly below it? isReplyDirectlyBelow: function(post) { - var postBelow, posts; - posts = this.get('postStream.posts'); + var posts = this.get('postStream.posts'); if (!posts) return; - postBelow = posts[posts.indexOf(post) + 1]; + var postBelow = posts[posts.indexOf(post) + 1]; // If the post directly below's reply_to_post_number is our post number, it's // considered directly below. return postBelow && postBelow.get('reply_to_post_number') === post.get('post_number'); }, - hasExcerpt: function() { - return this.get('pinned') && this.get('excerpt') && this.get('excerpt').length > 0; - }.property('pinned', 'excerpt'), + excerptNotEmpty: Em.computed.notEmpty('excerpt'), + hasExcerpt: Em.computed.and('pinned', 'excerptNotEmpty'), excerptTruncated: function() { var e = this.get('excerpt'); return( e && e.substr(e.length - 8,8) === '…' ); }.property('excerpt'), - canClearPin: function() { - return this.get('pinned') && (this.get('last_read_post_number') === this.get('highest_post_number')); - }.property('pinned', 'last_read_post_number', 'highest_post_number') + readLastPost: Discourse.computed.propertyEqual('last_read_post_number', 'highest_post_number'), + canCleanPin: Em.computed.and('pinned', 'readLastPost') + }); Discourse.Topic.reopenClass({ diff --git a/app/assets/javascripts/discourse/models/user.js b/app/assets/javascripts/discourse/models/user.js index 96d80aa708c..5d675da491f 100644 --- a/app/assets/javascripts/discourse/models/user.js +++ b/app/assets/javascripts/discourse/models/user.js @@ -8,15 +8,23 @@ **/ Discourse.User = Discourse.Model.extend({ + /** + Is this user a member of staff? + + @property staff + @type {Boolean} + **/ + staff: Em.computed.or('admin', 'moderator'), + /** Large version of this user's avatar. @property avatarLarge @type {String} **/ - avatarLarge: (function() { + avatarLarge: function() { return Discourse.Utilities.avatarUrl(this.get('username'), 'large', this.get('avatar_template')); - }).property('username'), + }.property('username'), /** Small version of this user's avatar. @@ -39,11 +47,10 @@ Discourse.User = Discourse.Model.extend({ @type {String} **/ websiteName: function() { - return this.get('website').split("/")[2]; - }.property('website'), + var website = this.get('website'); + if (Em.isEmpty(website)) { return; } - hasWebsite: function() { - return this.present('website'); + return this.get('website').split("/")[2]; }.property('website'), statusIcon: function() { @@ -65,9 +72,7 @@ Discourse.User = Discourse.Model.extend({ @property path @type {String} **/ - path: function() { - return Discourse.getURL("/users/") + (this.get('username_lower')); - }.property('username'), + path: Discourse.computed.url('username_lower', "/users/%@"), /** Path to this user's administration @@ -75,9 +80,7 @@ Discourse.User = Discourse.Model.extend({ @property adminPath @type {String} **/ - adminPath: function() { - return Discourse.getURL("/admin/users/") + (this.get('username_lower')); - }.property('username'), + adminPath: Discourse.computed.url('username_lower', "/admin/users/%@"), /** This user's username in lowercase. diff --git a/app/assets/javascripts/discourse/templates/user/activity.js.handlebars b/app/assets/javascripts/discourse/templates/user/activity.js.handlebars index 6daa7894fdd..9d4f125b50e 100644 --- a/app/assets/javascripts/discourse/templates/user/activity.js.handlebars +++ b/app/assets/javascripts/discourse/templates/user/activity.js.handlebars @@ -22,7 +22,7 @@ </ul> <div class='show'> <dl> - {{#if hasWebsite}} + {{#if websiteName}} <dt>{{i18n user.website}}:</dt><dd><a {{bindAttr href="website"}} target="_blank">{{websiteName}}</a></dd> {{/if}} {{#if created_at}} diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index a79a06ae02c..92d50c520b0 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -192,15 +192,15 @@ class PostsController < ApplicationController def create_params permitted = [ - :raw, - :topic_id, - :title, - :archetype, - :category, - :target_usernames, - :reply_to_post_number, - :image_sizes, - :auto_close_days + :raw, + :topic_id, + :title, + :archetype, + :category, + :target_usernames, + :reply_to_post_number, + :image_sizes, + :auto_close_days ] if api_key_valid? diff --git a/test/javascripts/components/computed_test.js b/test/javascripts/components/computed_test.js new file mode 100644 index 00000000000..000ea5fd6a7 --- /dev/null +++ b/test/javascripts/components/computed_test.js @@ -0,0 +1,50 @@ +module("Discourse.Computed"); + +var testClass = Em.Object.extend({ + same: Discourse.computed.propertyEqual('cookies', 'biscuits'), + exclaimyUsername: Discourse.computed.fmt('username', "!!! %@ !!!"), + multiple: Discourse.computed.fmt('username', 'mood', "%@ is %@"), + userUrl: Discourse.computed.url('username', "/users/%@") +}); + +test("propertyEqual", function() { + var t = testClass.create({ + cookies: 10, + biscuits: 10 + }); + + ok(t.get('same'), "it is true when the properties are the same"); + + t.set('biscuits', 9); + ok(!t.get('same'), "it isn't true when one property is different"); +}); + + +test("fmt", function() { + var t = testClass.create({ + username: 'eviltrout', + mood: "happy" + }); + + equal(t.get('exclaimyUsername'), '!!! eviltrout !!!', "it inserts the string"); + equal(t.get('multiple'), "eviltrout is happy"); + + t.set('username', 'codinghorror'); + equal(t.get('multiple'), "codinghorror is happy", "supports changing proerties"); + t.set('mood', 'ecstatic'); + equal(t.get('multiple'), "codinghorror is ecstatic", "supports changing another property"); +}); + + +test("url without a prefix", function() { + var t = testClass.create({ username: 'eviltrout' }); + equal(t.get('userUrl'), "/users/eviltrout"); + +}); + +test("url with a prefix", function() { + Discourse.BaseUri = "/prefixed/"; + var t = testClass.create({ username: 'eviltrout' }); + equal(t.get('userUrl'), "/prefixed/users/eviltrout"); + +}); \ No newline at end of file diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index 793546b94df..b661d09b312 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -80,5 +80,6 @@ Discourse.Router.map(function() { QUnit.testStart(function() { // Allow our tests to change site settings and have them reset before the next test Discourse.SiteSettings = jQuery.extend(true, {}, Discourse.SiteSettingsOriginal); + Discourse.BaseUri = "/"; })