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) === '&hellip;' );
   }.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 = "/";
 })