Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Kris Aubuchon 2014-02-11 00:44:10 -05:00
commit 24f1832cca
56 changed files with 727 additions and 390 deletions

View File

@ -1,3 +1,15 @@
GIT
remote: https://github.com/dysania/onebox.git
revision: 6a2f6e6a08f183a4df52f9a51187f566b8ae3a00
specs:
onebox (1.1.0)
hexpress (~> 1.2)
moneta (~> 0.7)
multi_json (~> 1.7)
mustache (~> 0.99)
nokogiri (~> 1.6.1)
opengraph_parser (~> 0.2.3)
GIT
remote: https://github.com/rails/actionpack-action_caching.git
revision: a45e97298f6a77a4d74662521715d5656b821f24
@ -7,47 +19,47 @@ GIT
GIT
remote: https://github.com/rails/rails.git
revision: 4aae538d9ffff3a00a81f3da52fa70f7fd79ac74
revision: 3a428f38b2f9a1e995070a4a049645b622c7094a
specs:
actionmailer (4.1.0.beta)
actionpack (= 4.1.0.beta)
actionview (= 4.1.0.beta)
actionmailer (4.1.0.beta1)
actionpack (= 4.1.0.beta1)
actionview (= 4.1.0.beta1)
mail (~> 2.5.4)
actionpack (4.1.0.beta)
actionview (= 4.1.0.beta)
activesupport (= 4.1.0.beta)
actionpack (4.1.0.beta1)
actionview (= 4.1.0.beta1)
activesupport (= 4.1.0.beta1)
rack (~> 1.5.2)
rack-test (~> 0.6.2)
actionview (4.1.0.beta)
activesupport (= 4.1.0.beta)
builder (~> 3.1.0)
actionview (4.1.0.beta1)
activesupport (= 4.1.0.beta1)
builder (~> 3.1)
erubis (~> 2.7.0)
activemodel (4.1.0.beta)
activesupport (= 4.1.0.beta)
builder (~> 3.1.0)
activerecord (4.1.0.beta)
activemodel (= 4.1.0.beta)
activesupport (= 4.1.0.beta)
activemodel (4.1.0.beta1)
activesupport (= 4.1.0.beta1)
builder (~> 3.1)
activerecord (4.1.0.beta1)
activemodel (= 4.1.0.beta1)
activesupport (= 4.1.0.beta1)
arel (~> 5.0.0)
activesupport (4.1.0.beta)
i18n (~> 0.6, >= 0.6.4)
activesupport (4.1.0.beta1)
i18n (~> 0.6, >= 0.6.9)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.1)
tzinfo (~> 1.1)
rails (4.1.0.beta)
actionmailer (= 4.1.0.beta)
actionpack (= 4.1.0.beta)
actionview (= 4.1.0.beta)
activemodel (= 4.1.0.beta)
activerecord (= 4.1.0.beta)
activesupport (= 4.1.0.beta)
rails (4.1.0.beta1)
actionmailer (= 4.1.0.beta1)
actionpack (= 4.1.0.beta1)
actionview (= 4.1.0.beta1)
activemodel (= 4.1.0.beta1)
activerecord (= 4.1.0.beta1)
activesupport (= 4.1.0.beta1)
bundler (>= 1.3.0, < 2.0)
railties (= 4.1.0.beta)
railties (= 4.1.0.beta1)
sprockets-rails (~> 2.0.0)
railties (4.1.0.beta)
actionpack (= 4.1.0.beta)
activesupport (= 4.1.0.beta)
railties (4.1.0.beta1)
actionpack (= 4.1.0.beta1)
activesupport (= 4.1.0.beta1)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
@ -84,7 +96,7 @@ GEM
erubis (>= 2.6.6)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
builder (3.1.4)
builder (3.2.2)
celluloid (0.15.2)
timers (~> 1.1.0)
certified (0.1.1)
@ -144,16 +156,15 @@ GEM
fspath (2.0.5)
given_core (3.1.1)
sorcerer (>= 0.3.7)
guess_html_encoding (0.0.9)
handlebars-source (1.1.2)
hashie (2.0.5)
hexpress (1.2.0)
highline (1.6.20)
hike (1.2.3)
hiredis (0.4.5)
html_truncator (0.3.1)
nokogiri (~> 1.5)
httpauth (0.2.0)
i18n (0.6.9)
ice_cube (0.11.1)
image_optim (0.9.1)
exifr (~> 1.1.3)
fspath (~> 2.0.5)
@ -186,19 +197,20 @@ GEM
metaclass (0.0.1)
method_source (0.8.2)
mime-types (1.25.1)
mini_portile (0.5.1)
minitest (5.1.0)
mini_portile (0.5.2)
minitest (5.2.2)
mocha (0.14.0)
metaclass (~> 0.0.1)
mock_redis (0.9.0)
moneta (0.7.20)
msgpack (0.5.7)
multi_json (1.8.2)
multi_json (1.8.4)
multipart-post (1.2.0)
mustache (0.99.4)
net-scp (1.1.2)
net-ssh (>= 2.6.5)
net-ssh (2.7.0)
nokogiri (1.6.0)
nokogiri (1.6.1)
mini_portile (~> 0.5.0)
oauth (0.4.7)
oauth2 (0.8.1)
@ -236,6 +248,9 @@ GEM
omniauth-twitter (1.0.1)
multi_json (~> 1.3)
omniauth-oauth (~> 1.0)
opengraph_parser (0.2.3)
addressable
nokogiri
openid-redis-store (0.0.2)
redis
ruby-openid
@ -269,7 +284,7 @@ GEM
rails-observers (0.1.2)
activemodel (~> 4.0)
raindrops (0.12.0)
rake (10.1.0)
rake (10.1.1)
rake-compiler (0.9.2)
rake
rb-fsevent (0.9.3)
@ -281,24 +296,8 @@ GEM
trollop (>= 1.16.2)
redcarpet (3.0.0)
redis (3.0.6)
redis-actionpack (4.0.0)
actionpack (~> 4)
redis-rack (~> 1.5.0)
redis-store (~> 1.1.0)
redis-activesupport (4.0.0)
activesupport (~> 4)
redis-store (~> 1.1.0)
redis-namespace (1.3.2)
redis (~> 3.0.4)
redis-rack (1.5.0)
rack (~> 1.5)
redis-store (~> 1.1.0)
redis-rails (4.0.0)
redis-actionpack (~> 4)
redis-activesupport (~> 4)
redis-store (~> 1.1.0)
redis-store (1.1.4)
redis (>= 2.2)
ref (1.0.5)
rest-client (1.6.7)
mime-types (>= 1.16)
@ -323,6 +322,9 @@ GEM
rspec-mocks (~> 2.14.0)
ruby-hmac (0.4.0)
ruby-openid (2.3.0)
ruby-readability (0.6.0)
guess_html_encoding (>= 0.0.4)
nokogiri (>= 1.4.2)
sanitize (2.0.6)
nokogiri (>= 1.4.4)
sass (3.2.12)
@ -347,10 +349,7 @@ GEM
redis-namespace (>= 1.3.1)
sidekiq-failures (0.2.2)
sidekiq (>= 2.9.0)
sidetiq (0.4.3)
celluloid (>= 0.14.1)
ice_cube (~> 0.11.0)
sidekiq (~> 2.15.0)
simple-rss (1.3.1)
simplecov (0.7.1)
multi_json (~> 1.0)
simplecov-html (~> 0.7.1)
@ -378,7 +377,7 @@ GEM
activesupport (>= 3.0)
sprockets (~> 2.8)
temple (0.6.7)
therubyracer-discourse (0.12.0)
therubyracer (0.12.1)
libv8 (~> 3.16.14.0)
ref
thin (1.6.1)
@ -436,7 +435,6 @@ DEPENDENCIES
handlebars-source (~> 1.1.2)
highline
hiredis
html_truncator
image_optim
image_sorcery
librarian (>= 0.0.25)
@ -458,6 +456,7 @@ DEPENDENCIES
omniauth-oauth2
omniauth-openid
omniauth-twitter
onebox!
openid-redis-store
pg (= 0.15.1)
pry-nav
@ -465,7 +464,7 @@ DEPENDENCIES
puma
qunit-rails
rack-cors
rack-mini-profiler (= 0.9.0.pre)
rack-mini-profiler
rack-protection
rails!
rails-observers
@ -476,11 +475,11 @@ DEPENDENCIES
rbtrace
redcarpet
redis
redis-rails
rest-client
rinku
rspec-given
rspec-rails
ruby-readability
sanitize
sass
sass-rails
@ -488,12 +487,12 @@ DEPENDENCIES
shoulda
sidekiq (= 2.15.1)
sidekiq-failures
sidetiq (>= 0.3.6)
simple-rss
simplecov
sinatra
slim
spork-rails
therubyracer-discourse
therubyracer
thin
timecop
uglifier

View File

@ -24,6 +24,8 @@ Discourse.AdminUserIndexController = Discourse.ObjectController.extend({
return Discourse.SiteSettings.must_approve_users;
}.property(),
primaryGroupDirty: Discourse.computed.propertyNotEqual('originalPrimaryGroupId', 'primary_group_id'),
actions: {
toggleTitleEdit: function() {
this.toggleProperty('editingTitle');
@ -44,6 +46,22 @@ Discourse.AdminUserIndexController = Discourse.ObjectController.extend({
this.get('model').generateApiKey();
},
savePrimaryGroup: function() {
var self = this;
Discourse.ajax("/admin/users/" + this.get('id') + "/primary_group", {
type: 'PUT',
data: {primary_group_id: this.get('primary_group_id')}
}).then(function () {
self.set('originalPrimaryGroupId', self.get('primary_group_id'));
}).catch(function() {
bootbox.alert(I18n.t('generic_error'));
});
},
resetPrimaryGroup: function() {
this.set('primary_group_id', this.get('originalPrimaryGroupId'));
},
regenerateApiKey: function() {
var self = this;
bootbox.confirm(I18n.t("admin.api.confirm_regen"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {

View File

@ -19,7 +19,7 @@ Discourse.AdminDashboard.reopenClass({
@return {jqXHR} a jQuery Promise object
**/
find: function() {
return Discourse.ajax("/admin/dashboard").then(function(json) {
return Discourse.ajax("/admin/dashboard.json").then(function(json) {
var model = Discourse.AdminDashboard.create(json);
model.set('loaded', true);
return model;
@ -34,7 +34,7 @@ Discourse.AdminDashboard.reopenClass({
@return {jqXHR} a jQuery Promise object
**/
fetchProblems: function() {
return Discourse.ajax("/admin/dashboard/problems", {
return Discourse.ajax("/admin/dashboard/problems.json", {
type: 'GET',
dataType: 'json'
}).then(function(json) {

View File

@ -388,7 +388,7 @@ Discourse.AdminUser.reopenClass({
},
find: function(username) {
return Discourse.ajax("/admin/users/" + username).then(function (result) {
return Discourse.ajax("/admin/users/" + username + ".json").then(function (result) {
result.loadedDetails = true;
return Discourse.AdminUser.create(result);
});

View File

@ -23,10 +23,16 @@ Discourse.AdminUserRoute = Discourse.Route.extend({
afterModel: function(adminUser) {
var controller = this.controllerFor('adminUser');
adminUser.loadDetails().then(function () {
return adminUser.loadDetails().then(function () {
adminUser.setOriginalTrustLevel();
controller.set('model', adminUser);
window.scrollTo(0, 0);
});
},
setupController: function(controller, model) {
controller.setProperties({
originalPrimaryGroupId: model.get('primary_group_id'),
model: model
});
},

View File

@ -46,6 +46,25 @@
</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n admin.groups.primary}}</div>
<div class='value'>
{{#if custom_groups}}
{{combobox content=custom_groups value=primary_group_id nameProperty="name" none="admin.groups.no_primary"}}
{{else}}
&mdash;
{{/if}}
</div>
<div class='controls'>
{{#if primaryGroupDirty}}
<div>
<button class='btn ok' {{action savePrimaryGroup}}><i class='fa fa-check'></i></button>
<button class='btn cancel' {{action resetPrimaryGroup}}><i class='fa fa-times'></i></button>
</div>
{{/if}}
</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n user.ip_address.title}}</div>
<div class='value'>{{ip_address}}</div>
@ -317,7 +336,7 @@
<section>
<hr/>
<button {{bind-attr class=":btn :btn-danger :pull-right deleteForbidden:hidden"}} {{action destroy target="content"}} {{bind-attr disabled="deleteForbidden"}} {{bind-attr}}>
<button {{bind-attr class=":btn :btn-danger :pull-right deleteForbidden:hidden"}} {{action destroy target="content"}} {{bind-attr disabled="deleteForbidden"}}>
<i class="fa fa-exclamation-triangle"></i>
{{i18n admin.user.delete}}
</button>

View File

@ -0,0 +1,10 @@
/**
The view class for an Admin User
@class AdminUserView
@extends Discourse.View
@namespace Discourse
@module Discourse
**/
Discourse.AdminUserView = Discourse.View.extend(Discourse.ScrollTop);

View File

@ -0,0 +1,12 @@
/**
Displays a list of groups that a user belongs to.
@class Discourse.GroupsListComponent
@extends Ember.Component
@namespace Discourse
@module Discourse
**/
Discourse.GroupsListComponent = Em.Component.extend({
classNames: ['groups']
});

View File

@ -25,7 +25,10 @@ Discourse.PostGapComponent = Ember.Component.extend({
if (this.get('loading')) {
buffer.push(I18n.t('loading'));
} else {
buffer.push(I18n.t('post.gap', {count: this.get('gap.length')}));
var gapLength = this.get('gap.length');
if (gapLength) {
buffer.push(I18n.t('post.gap', {count: gapLength}));
}
}
},

View File

@ -29,7 +29,7 @@ Discourse.StaticController = Discourse.Controller.extend({
text = text.match(/<!-- preload-content: -->((?:.|[\n\r])*)<!-- :preload-content -->/)[1];
this.set('content', text);
} else {
return Discourse.ajax(path, {dataType: 'html'}).then(function (result) {
return Discourse.ajax(path + ".html", {dataType: 'html'}).then(function (result) {
self.set('content', result);
});
}

View File

@ -37,6 +37,11 @@ Discourse.ScreenTrack = Ember.Object.extend({
},
stop: function() {
if(!this.get('topicId')) {
// already stopped no need to "extra stop"
return;
}
this.tick();
this.flush();
this.reset();
@ -105,9 +110,10 @@ Discourse.ScreenTrack = Ember.Object.extend({
var highestSeenByTopic = Discourse.Session.currentProp('highestSeenByTopic');
if ((highestSeenByTopic[topicId] || 0) < highestSeen) {
highestSeenByTopic[topicId] = highestSeen;
Discourse.TopicTrackingState.current().updateSeen(topicId, highestSeen);
}
Discourse.TopicTrackingState.current().updateSeen(topicId, highestSeen);
if (!$.isEmptyObject(newTimings)) {
Discourse.ajax('/topics/timings', {
data: {

View File

@ -0,0 +1,18 @@
/**
This mixin will cause a view to scroll the viewport to the top once it has been inserted
@class Discourse.ScrollTop
@extends Ember.Mixin
@namespace Discourse
@module Discourse
**/
Discourse.ScrollTop = Em.Mixin.create({
_scrollTop: function() {
Em.run.schedule('afterRender', function() {
$(document).scrollTop(0);
});
}.on('didInsertElement'),
});

View File

@ -39,8 +39,9 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
},
updateSeen: function(topicId, highestSeen) {
if(!topicId || !highestSeen) { return; }
var state = this.states["t" + topicId];
if(state && state.last_read_post_number < highestSeen) {
if(state && (!state.last_read_post_number || state.last_read_post_number < highestSeen)) {
state.last_read_post_number = highestSeen;
this.incrementMessageCount();
}
@ -84,9 +85,24 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
sync: function(list, filter){
var tracker = this;
var states = this.states;
if(!list || !list.topics) { return; }
// compensate for delayed "new" topics
// client side we know they are not new, server side we think they are
for(var i=list.topics.length-1; i>=0; i--){
var state = states["t"+ list.topics[i].id];
if(state && state.last_read_post_number > 0){
if(filter === "new"){
list.topics.splice(i, 1);
} else {
list.topics[i].unseen = false;
list.topics[i].dont_sync = true;
}
}
}
if(filter === "new" && !list.more_topics_url){
// scrub all new rows and reload from list
_.each(this.states, function(state){
@ -112,10 +128,11 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
if(topic.unseen) {
row.last_read_post_number = null;
} else if (topic.unread || topic.new_posts){
// subtle issue here
row.last_read_post_number = topic.highest_post_number - ((topic.unread||0) + (topic.new_posts||0));
} else {
if(!topic.dont_sync) {
delete tracker.states["t" + topic.id];
}
return;
}

View File

@ -11,6 +11,9 @@ function buildTopicRoute(filter) {
},
model: function() {
// attempt to stop early cause we need this to be called before .sync
Discourse.ScreenTrack.current().stop();
return Discourse.TopicList.list(filter).then(function(list) {
var tracking = Discourse.TopicTrackingState.current();
if (tracking) {

View File

@ -0,0 +1,6 @@
{{#if groups}}
{{i18n groups.title count=groups.length}}:
{{#each groups}}
{{#link-to 'group' this class="group-link"}}{{name}}{{/link-to}}
{{/each}}
{{/if}}

View File

@ -29,7 +29,7 @@
{{/each}}
{{else}}
<label>{{i18n category.parent}}</label>
{{categoryChooser valueAttribute="id" value=parent_category_id categories=parentCategories}}
{{categoryChooser valueAttribute="id" value=parent_category_id categories=parentCategories rootNone=true}}
{{/if}}
</section>

View File

@ -29,6 +29,7 @@
{{/if}}
{{#if user_title}}<div class="user-title" {{action showPosterExpansion this}}>{{user_title}}</div>{{/if}}
{{#if primary_group_name}}<div><a href='/groups/{{unbound primary_group_name}}' class='user-group'>{{unbound primary_group_name}}</a></div>{{/if}}
</div>
{{else}}
<div class="contents">

View File

@ -10,6 +10,8 @@
<h3>{{i18n last_post}} {{unboundDate path="user.last_posted_at" leaveAgo="true"}}</h3>
<h3>{{i18n joined}} {{unboundDate path="user.created_at" leaveAgo="true"}}</h3>
{{groups-list groups=user.custom_groups}}
<div class='bottom'>
{{#if user.bio_cooked}}<div class='bio'>{{{user.bio_cooked}}}</div>{{/if}}

View File

@ -43,14 +43,7 @@
<div class='bio'>{{{bio_cooked}}}</div>
{{#if custom_groups}}
<div class='groups'>
{{i18n groups.title count=custom_groups.length}}:
{{#each custom_groups}}
{{#link-to 'group' this class="group-link"}}{{name}}{{/link-to}}
{{/each}}
</div>
{{/if}}
{{groups-list groups=custom_groups}}
{{#if isSuspended}}
<div class='suspended'>

View File

@ -26,7 +26,11 @@ Discourse.CategoryChooserView = Discourse.ComboboxView.extend({
none: function() {
if (Discourse.User.currentProp('staff') || Discourse.SiteSettings.allow_uncategorized_topics) {
return 'category.none';
if (this.get('rootNone')) {
return "category.none";
} else {
return Discourse.Category.list().findBy('id', Discourse.Site.currentProp('uncategorized_category_id'));
}
} else {
return 'category.choose';
}

View File

@ -11,41 +11,45 @@ Discourse.ComboboxView = Discourse.View.extend({
classNames: ['combobox'],
valueAttribute: 'id',
render: function(buffer) {
buildData: function(o) {
var data = "";
if (this.dataAttributes) {
this.dataAttributes.forEach(function(a) {
data += "data-" + a + "=\"" + o.get(a) + "\" ";
});
}
return data;
},
var nameProperty = this.get('nameProperty') || 'name';
render: function(buffer) {
var nameProperty = this.get('nameProperty') || 'name',
none = this.get('none');
// Add none option if required
if (this.get('none')) {
buffer.push('<option value="">' + (I18n.t(this.get('none'))) + "</option>");
if (typeof none === "string") {
buffer.push('<option value="">' + I18n.t(none) + "</option>");
} else if (typeof none === "object") {
buffer.push("<option value=\"\" " + this.buildData(none) + ">" + Em.get(none, nameProperty) + "</option>");
}
var selected = this.get('value');
if (selected) { selected = selected.toString(); }
if (this.get('content')) {
var comboboxView = this;
_.each(this.get('content'),function(o) {
var val = o[comboboxView.get('valueAttribute')];
var self = this;
this.get('content').forEach(function(o) {
var val = o[self.get('valueAttribute')];
if (val) { val = val.toString(); }
var selectedText = (val === selected) ? "selected" : "";
var data = "";
if (comboboxView.dataAttributes) {
comboboxView.dataAttributes.forEach(function(a) {
data += "data-" + a + "=\"" + o.get(a) + "\" ";
});
}
buffer.push("<option " + selectedText + " value=\"" + val + "\" " + data + ">" + Em.get(o, nameProperty) + "</option>");
buffer.push("<option " + selectedText + " value=\"" + val + "\" " + self.buildData(o) + ">" + Em.get(o, nameProperty) + "</option>");
});
}
},
valueChanged: function() {
var $combo = this.$();
var val = this.get('value');
var $combo = this.$(),
val = this.get('value');
if (val !== undefined && val !== null) {
$combo.val(val.toString());
} else {
@ -55,8 +59,8 @@ Discourse.ComboboxView = Discourse.View.extend({
}.observes('value'),
didInsertElement: function() {
var $elem = this.$();
var comboboxView = this;
var $elem = this.$(),
self = this;
$elem.chosen({ template: this.template, disable_search_threshold: 5 });
if (this.overrideWidths) {
@ -74,7 +78,7 @@ Discourse.ComboboxView = Discourse.View.extend({
}
$elem.chosen().change(function(e) {
comboboxView.set('value', $(e.target).val());
self.set('value', $(e.target).val());
});
}

View File

@ -7,15 +7,9 @@
@namespace Discourse
@module Discourse
**/
Discourse.DiscoveryTopicsView = Discourse.View.extend(Discourse.LoadMore, {
Discourse.DiscoveryTopicsView = Discourse.View.extend(Discourse.ScrollTop, Discourse.LoadMore, {
eyelineSelector: '.topic-list-item',
_scrollTop: function() {
Em.run.schedule('afterRender', function() {
$(document).scrollTop(0);
});
}.on('didInsertElement'),
actions: {
loadMore: function() {
var self = this;

View File

@ -1,4 +1,13 @@
Discourse.GroupIndexView = Discourse.View.extend(Discourse.LoadMore, {
/**
Displays all posts within a group
@class Discourse.GroupIndexView
@extends Ember.Mixin
@namespace Discourse
@module Discourse
**/
Discourse.GroupIndexView = Discourse.View.extend(Discourse.ScrollTop, Discourse.LoadMore, {
eyelineSelector: '.user-stream .item'
});

View File

@ -12,13 +12,21 @@ Discourse.PostView = Discourse.GroupedView.extend(Ember.Evented, {
classNameBindings: ['postTypeClass',
'selected',
'post.hidden:post-hidden',
'post.deleted'],
'post.deleted',
'groupNameClass'],
postBinding: 'content',
postTypeClass: function() {
return this.get('post.post_type') === Discourse.Site.currentProp('post_types.moderator_action') ? 'moderator' : 'regular';
}.property('post.post_type'),
groupNameClass: function() {
var primaryGroupName = this.get('post.primary_group_name');
if (primaryGroupName) {
return "group-" + primaryGroupName;
}
}.property('post.primary_group_name'),
// If the cooked content changed, add the quote controls
cookedChanged: function() {
var postView = this;

View File

@ -40,6 +40,16 @@
margin-top: 0px;
color: $primary_medium;
}
.groups {
font-size: 13px;
font-weight: normal;
margin-top: 0px;
color: $primary_medium;
.group-link {
color: $primary;
}
}
.bottom {
clear: both;

View File

@ -472,6 +472,15 @@ iframe {
width: 45px;
}
a.user-group {
margin: 4px 0 0 0;
padding: 0px;
color: $primary_light;
font-size: 80%;
width: 100%;
line-height: 13px;
}
h3 a {
display: inline;
width: auto;
@ -614,6 +623,7 @@ position: relative;
}
}
.user-title {
margin-top: 8px;
color: $primary_light;

View File

@ -2,6 +2,12 @@
@import "common/foundation/variables";
@import "common/foundation/mixins";
.groups {
.group-link {
color: $tertiary_lightest;
}
}
.user-preferences {
input.category-group {
width: 500px;
@ -210,9 +216,6 @@
h1, h2 {margin-top: 10px;}
.group-link {
color: $tertiary_lightest;
}
.bio {
color: $primary_lighter;

View File

@ -442,6 +442,7 @@ iframe {
float: left;
}
.user-title {
color: #aaa;
padding-top: 2px;

View File

@ -17,6 +17,7 @@ class Admin::UsersController < Admin::AdminController
:block,
:unblock,
:trust_level,
:primary_group,
:generate_api_key,
:revoke_api_key]
@ -94,6 +95,13 @@ class Admin::UsersController < Admin::AdminController
render_serialized(@user, AdminUserSerializer)
end
def primary_group
guardian.ensure_can_change_primary_group!(@user)
@user.primary_group_id = params[:primary_group_id]
@user.save!
render nothing: true
end
def trust_level
guardian.ensure_can_change_trust_level!(@user)
logger = StaffActionLogger.new(current_user)

View File

@ -40,6 +40,11 @@ class ListController < ApplicationController
list_opts = build_topic_list_options
list_opts.merge!(options) if options
user = list_target_user
if filter == :latest && params[:category].blank?
list_opts[:no_definitions] = true
end
list = TopicQuery.new(user, list_opts).public_send("list_#{filter}")
list.more_topics_url = construct_url_with(list_opts)
if Discourse.anonymous_filters.include?(filter)

View File

@ -30,7 +30,8 @@ module Jobs
)', post.topic.category_id, CategoryUser.notification_levels[:muted])
.each do |user|
if Guardian.new(user).can_see?(post)
UserNotifications.mailing_list_notify(user, post).deliver
message = UserNotifications.mailing_list_notify(user, post)
Email::Sender.new(message, :mailing_list, user).send
end
end

View File

@ -1,7 +1,7 @@
module Jobs
# various consistency checks
class EnsureDbConsistency < Jobs::Scheduled
every 1.day
every 12.hours
def execute(args)
TopicUser.ensure_consistency!

View File

@ -220,6 +220,7 @@ class Group < ActiveRecord::Base
if @deletions
@deletions.each do |gu|
gu.destroy
User.update_all 'primary_group_id = NULL', ['id = ? AND primary_group_id = ?', gu.user_id, gu.group_id]
end
end
@deletions = nil

View File

@ -14,12 +14,14 @@ class AdminDetailedUserSerializer < AdminUserSerializer
:private_topics_count,
:can_delete_all_posts,
:can_be_deleted,
:suspend_reason
:suspend_reason,
:primary_group_id
has_one :approved_by, serializer: BasicUserSerializer, embed: :objects
has_one :api_key, serializer: ApiKeySerializer, embed: :objects
has_one :suspended_by, serializer: BasicUserSerializer, embed: :objects
has_one :leader_requirements, serializer: LeaderRequirementsSerializer, embed: :objects
has_many :custom_groups, embed: :object, serializer: BasicGroupSerializer
def can_revoke_admin
scope.can_revoke_admin?(object)

View File

@ -29,31 +29,36 @@ class ListableTopicSerializer < BasicTopicSerializer
end
def seen
object.user_data.present?
return true if !scope || !scope.user
return true if object.user_data && !object.user_data.last_read_post_number.nil?
return true if object.created_at < scope.user.treat_as_new_topic_start_date
false
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
!seen
end
def last_read_post_number
return nil unless object.user_data
object.user_data.last_read_post_number
end
alias :include_last_read_post_number? :seen
def has_user_data
!!object.user_data
end
alias :include_last_read_post_number? :has_user_data
def unread
unread_helper.unread_posts
end
alias :include_unread? :seen
alias :include_unread? :has_user_data
def new_posts
unread_helper.new_posts
end
alias :include_new_posts? :seen
alias :include_new_posts? :has_user_data
def include_excerpt?
pinned

View File

@ -23,6 +23,7 @@ class PostSerializer < BasicPostSerializer
:topic_slug,
:topic_id,
:display_username,
:primary_group_name,
:version,
:can_edit,
:can_delete,
@ -75,6 +76,11 @@ class PostSerializer < BasicPostSerializer
object.user.try(:name)
end
def primary_group_name
return nil unless object.user && @topic_view
return @topic_view.primary_group_names[object.user.primary_group_id] if object.user.primary_group_id
end
def link_counts
return @single_post_link_counts if @single_post_link_counts.present?

View File

@ -13,7 +13,7 @@ class TopicListItemSerializer < ListableTopicSerializer
def starred
object.user_data.starred?
end
alias :include_starred? :seen
alias :include_starred? :has_user_data
def posters
object.posters || []

3
bin/bundle Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
load Gem.bin_path('bundler', 'bundle')

4
bin/rails Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'

4
bin/rake Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env ruby
require_relative '../config/boot'
require 'rake'
Rake.application.run

View File

@ -1267,6 +1267,8 @@ en:
other: "spam x{{count}}"
groups:
primary: "Primary Group"
no_primary: "(no primary group)"
title: "Groups"
edit: "Edit Groups"
selector_placeholder: "add users"

View File

@ -656,7 +656,23 @@ fr:
read_more: "Vous voulez en lire plus? {{catLink}} or {{latestLink}}."
# keys ending with _MF use message format, see /spec/components/js_local_helper_spec.rb for samples
read_more_MF: "Il y a { UNREAD, plural, =0 {} one { is <a href='/unread'>1 unread</a> } other { are <a href='/unread'># unread</a> } } { NEW, plural, =0 {} one { {BOTH, select, true{and } false {is } other{}} <a href='/new'>1 new</a> topic} other { {BOTH, select, true{and } false {are } other{}} <a href='/new'># new</a> topics} } remaining, or {CATEGORY, select, true {browse other topics in {catLink}} false {{latestLink}} other {}}."
read_more_MF: "Il y a {
UNREAD, plural,
=0 {}
one {
<a href='/unread'>1 discussion non-lue</a>
} other {
<a href='/unread'># discussions non-lues</a>
}
} {
NEW, plural,
=0 {}
one {
{BOTH, select, true{et } false {} other{}} <a href='/new'>1 nouvelle</a> discussion
} other {
{BOTH, select, true{et } false {} other{}} <a href='/new'># nouvelles</a> discussions
}
} à lire, ou {CATEGORY, select, true {allez voir les nouvelles discussions dans {catLink}} false {{latestLink}} other {}}."
browse_all_categories: Voir toutes les catégories
@ -1056,6 +1072,8 @@ fr:
categories_list: "Liste des Catégories"
filters:
with_topics: "Discussions %{filter}"
with_category: "Discussions %{filter} dans %{category}"
latest:
title: "Récentes"
help: "discussions récentes"

View File

@ -58,6 +58,7 @@ Discourse::Application.routes.draw do
put "block"
put "unblock"
put "trust_level"
put "primary_group"
get "leader_requirements"
end

View File

@ -0,0 +1,5 @@
class AddPrimaryGroupIdToUsers < ActiveRecord::Migration
def change
add_column :users, :primary_group_id, :integer, null: true
end
end

View File

@ -133,6 +133,10 @@ class Guardian
user && is_staff?
end
def can_change_primary_group?(user)
user && is_staff?
end
def can_change_trust_level?(user)
user && is_staff?
end

View File

@ -70,19 +70,10 @@ class PostCreator
@post.save_reply_relationships
end
if @spam
GroupMessage.create( Group[:moderators].name,
:spam_post_blocked,
{ user: @user,
limit_once_per: 24.hours,
message_params: {domains: @post.linked_hosts.keys.join(', ')} } )
elsif @post && !@post.errors.present? && !@opts[:skip_validations]
SpamRulesEnforcer.enforce!(@post)
end
handle_spam
track_latest_on_category
enqueue_jobs
@post
end
@ -92,9 +83,7 @@ class PostCreator
end
def self.before_create_tasks(post)
if post.reply_to_post_number.present?
post.reply_to_user_id ||= Post.select(:user_id).where(topic_id: post.topic_id, post_number: post.reply_to_post_number).first.try(:user_id)
end
set_reply_user_id(post)
post.word_count = post.raw.scan(/\w+/).size
post.post_number ||= Topic.next_post_number(post.topic_id, post.reply_to_post_number.present?)
@ -108,16 +97,31 @@ class PostCreator
post.last_version_at ||= Time.now
end
def self.set_reply_user_id(post)
return unless post.reply_to_post_number.present?
post.reply_to_user_id ||= Post.select(:user_id).where(topic_id: post.topic_id, post_number: post.reply_to_post_number).first.try(:user_id)
end
protected
def handle_spam
if @spam
GroupMessage.create( Group[:moderators].name,
:spam_post_blocked,
{ user: @user,
limit_once_per: 24.hours,
message_params: {domains: @post.linked_hosts.keys.join(', ')} } )
elsif @post && !@post.errors.present? && !@opts[:skip_validations]
SpamRulesEnforcer.enforce!(@post)
end
end
def track_latest_on_category
if @post && @post.errors.count == 0 && @topic && @topic.category_id
return unless @post && @post.errors.count == 0 && @topic && @topic.category_id
Category.where(id: @topic.category_id).update_all(latest_post_id: @post.id)
if @post.post_number == 1
Category.where(id: @topic.category_id).update_all(latest_topic_id: @topic.id)
end
end
Category.where(id: @topic.category_id).update_all(latest_topic_id: @topic.id) if @post.post_number == 1
end
def ensure_in_allowed_users
@ -135,10 +139,12 @@ class PostCreator
end
def after_post_create
if !@topic.private_message? && @post.post_type != Post.types[:moderator_action]
return if @topic.private_message? || @post.post_type == Post.types[:moderator_action]
if @post.post_number > 1
TopicTrackingState.publish_unread(@post)
end
if SiteSetting.enable_mailing_list_mode
Jobs.enqueue_in(
SiteSetting.email_time_window_mins.minutes,
@ -147,12 +153,11 @@ class PostCreator
)
end
end
end
def after_topic_create
return unless @new_topic
# Don't publish invisible topics
return unless @topic.visible?
return if @topic.private_message? || @post.post_type == Post.types[:moderator_action]
@topic.posters = @topic.posters_summary
@ -218,13 +223,13 @@ class PostCreator
reply_to_post_number: @opts[:reply_to_post_number])
# Attributes we pass through to the post instance if present
[:post_type, :no_bump, :cooking_options, :image_sizes, :acting_user, :invalidate_oneboxes].each do |a|
[:post_type, :no_bump, :cooking_options, :image_sizes, :acting_user, :invalidate_oneboxes, :cook_method].each do |a|
post.send("#{a}=", @opts[a]) if @opts[a].present?
end
post.cook_method = @opts[:cook_method] if @opts[:cook_method].present?
post.extract_quoted_post_numbers
post.created_at = Time.zone.parse(@opts[:created_at].to_s) if @opts[:created_at].present?
@post = post
end
@ -249,10 +254,10 @@ class PostCreator
end
def consider_clearing_flags
if @topic.private_message? && @post.post_number > 1 && @topic.user_id != @post.user_id
return unless @topic.private_message? && @post.post_number > 1 && @topic.user_id != @post.user_id
clear_possible_flags(@topic)
end
end
def update_user_counts
# We don't count replies to your own topics
@ -266,7 +271,8 @@ class PostCreator
end
def publish
if @post.post_number > 1
return unless @post.post_number > 1
MessageBus.publish("/topic/#{@post.topic_id}",{
id: @post.id,
created_at: @post.created_at,
@ -276,14 +282,14 @@ class PostCreator
group_ids: secure_group_ids(@topic)
)
end
end
def extract_links
TopicLink.extract_from(@post)
end
def track_topic
unless @opts[:auto_track] == false
return if @opts[:auto_track] == false
TopicUser.auto_track(@user.id, @topic.id, TopicUser.notification_reasons[:created_post])
# Update topic user data
TopicUser.change(@post.user.id,
@ -292,17 +298,16 @@ class PostCreator
last_read_post_number: @post.post_number,
seen_post_count: @post.post_number)
end
end
def enqueue_jobs
if @post && !@post.errors.present?
return unless @post && !@post.errors.present?
# We need to enqueue jobs after the transaction. Otherwise they might begin before the data has
# been comitted.
topic_id = @opts[:topic_id] || @topic.try(:id)
Jobs.enqueue(:feature_topic_users, topic_id: @topic.id) if topic_id.present?
@post.trigger_post_process
after_post_create
after_topic_create if @new_topic
end
after_topic_create
end
end

View File

@ -8,7 +8,15 @@ module Scheduler
RailsMultisite::ConnectionManagement.with_connection("default") do
@manager = Scheduler::Manager.without_runner
@schedules = Scheduler::Manager.discover_schedules.sort do |a,b|
a.schedule_info.next_run <=> b.schedule_info.next_run
a_next = a.schedule_info.next_run
b_next = b.schedule_info.next_run
if a_next && b_next
a_next <=> b_next
elsif a_next
-1
else
1
end
end
erb File.read(File.join(VIEWS, 'scheduler.erb')), locals: {view_path: VIEWS}
end

View File

@ -240,8 +240,8 @@ class TopicQuery
result = result.listable_topics.includes(category: :topic_only_relative_url)
result = result.where('categories.name is null or categories.name <> ?', options[:exclude_category]).references(:categories) if options[:exclude_category]
# Don't include the category topic unless restricted to that category
if options[:category].blank? || options[:no_definitions]
# Don't include the category topics if excluded
if options[:no_definitions]
result = result.where('COALESCE(categories.topic_id, 0) <> topics.id')
end

View File

@ -111,6 +111,22 @@ class TopicView
filter_posts_paged(opts[:page].to_i)
end
def primary_group_names
return @group_names if @group_names
primary_group_ids = Set.new
@posts.each do |p|
primary_group_ids << p.user.primary_group_id if p.user.try(:primary_group_id)
end
result = {}
unless primary_group_ids.empty?
Group.where(id: primary_group_ids.to_a).pluck(:id, :name).each do |g|
result[g[0]] = g[1]
end
end
result
end
# Find the sort order for a post in the topic
def sort_order_for_post_number(post_number)

View File

@ -25,7 +25,7 @@ describe Scheduler::Manager do
end
end
let(:manager) { Scheduler::Manager.new(Redis.new) }
let(:manager) { Scheduler::Manager.new(DiscourseRedis.new) }
before do
$redis.del manager.class.queue_key
@ -46,7 +46,7 @@ describe Scheduler::Manager do
(0..5).map do
Thread.new do
manager = Scheduler::Manager.new(Redis.new)
manager = Scheduler::Manager.new(DiscourseRedis.new)
manager.blocking_tick
end
end.map(&:join)

View File

@ -30,12 +30,12 @@ describe TopicQuery do
# mods can see hidden topics
TopicQuery.new(moderator).list_latest.topics.count.should == 1
# admins can see all the topics
TopicQuery.new(admin).list_latest.topics.count.should == 2
TopicQuery.new(admin).list_latest.topics.count.should == 3
group.add(user)
group.save
TopicQuery.new(user).list_latest.topics.count.should == 1
TopicQuery.new(user).list_latest.topics.count.should == 2
end

View File

@ -140,6 +140,29 @@ describe Admin::UsersController do
end
end
context '.primary_group' do
before do
@another_user = Fabricate(:coding_horror)
end
it "raises an error when the user doesn't have permission" do
Guardian.any_instance.expects(:can_change_primary_group?).with(@another_user).returns(false)
xhr :put, :primary_group, user_id: @another_user.id
response.should be_forbidden
end
it "returns a 404 if the user doesn't exist" do
xhr :put, :primary_group, user_id: 123123
response.should be_forbidden
end
it "changes the user's primary group" do
xhr :put, :primary_group, user_id: @another_user.id, primary_group_id: 2
@another_user.reload
@another_user.primary_group_id.should == 2
end
end
context '.trust_level' do
before do
@another_user = Fabricate(:coding_horror)

View File

@ -1050,4 +1050,35 @@ describe User do
end
end
describe "primary_group_id" do
let!(:user) { Fabricate(:user) }
it "has no primary_group_id by default" do
user.primary_group_id.should be_nil
end
context "when the user has a group" do
let!(:group) { Fabricate(:group) }
before do
group.usernames = user.username
group.save
user.primary_group_id = group.id
user.save
user.reload
end
it "should allow us to use it as a primary group" do
user.primary_group_id.should == group.id
# If we remove the user from the group
group.usernames = ""
group.save
# It should unset it from the primary_group_id
user.reload
user.primary_group_id.should be_nil
end
end
end
end

View File

@ -0,0 +1,21 @@
module("Discourse.TopicTrackingState");
test("sync", function () {
var state = Discourse.TopicTrackingState.create();
// fake track it
state.states["t111"] = {last_read_post_number: null};
state.updateSeen(111, 7);
var list = {topics: [{
highest_post_number: null,
id: 111,
unread: 10,
new_posts: 10
}]};
state.sync(list, "new");
equal(list.topics.length, 0, "expect new topic to be removed as it was seen");
});

View File

@ -5,7 +5,7 @@
* Portions Copyright 2008-2011 Apple Inc. All rights reserved.
* @license Licensed under MIT license
* See https://raw.github.com/emberjs/ember.js/master/LICENSE
* @version 1.3.2+pre.773be0ec
* @version 1.3.2
*/
@ -203,7 +203,7 @@ if (!Ember.testing) {
* Portions Copyright 2008-2011 Apple Inc. All rights reserved.
* @license Licensed under MIT license
* See https://raw.github.com/emberjs/ember.js/master/LICENSE
* @version 1.3.2+pre.773be0ec
* @version 1.3.2
*/
@ -286,7 +286,7 @@ var define, requireModule, require, requirejs;
@class Ember
@static
@version 1.3.2+pre.773be0ec
@version 1.3.2
*/
if ('undefined' === typeof Ember) {
@ -313,10 +313,10 @@ Ember.toString = function() { return "Ember"; };
/**
@property VERSION
@type String
@default '1.3.2+pre.773be0ec'
@default '1.3.2'
@static
*/
Ember.VERSION = '1.3.2+pre.773be0ec';
Ember.VERSION = '1.3.2';
/**
Standard environmental variables. You can define these in a global `EmberENV`
@ -1029,7 +1029,7 @@ Ember.guidFor = function guidFor(obj) {
// META
//
var META_DESC = Ember.META_DESC = {
var META_DESC = {
writable: true,
configurable: false,
enumerable: false,
@ -2505,7 +2505,7 @@ ObserverSet.prototype.clear = function() {
(function() {
var META_KEY = Ember.META_KEY,
var metaFor = Ember.meta,
guidFor = Ember.guidFor,
tryFinally = Ember.tryFinally,
sendEvent = Ember.sendEvent,
@ -2536,10 +2536,10 @@ var META_KEY = Ember.META_KEY,
@return {void}
*/
function propertyWillChange(obj, keyName) {
var m = obj[META_KEY],
watching = (m && m.watching[keyName] > 0) || keyName === 'length',
proto = m && m.proto,
desc = m && m.descs[keyName];
var m = metaFor(obj, false),
watching = m.watching[keyName] > 0 || keyName === 'length',
proto = m.proto,
desc = m.descs[keyName];
if (!watching) { return; }
if (proto === obj) { return; }
@ -2566,10 +2566,10 @@ Ember.propertyWillChange = propertyWillChange;
@return {void}
*/
function propertyDidChange(obj, keyName) {
var m = obj[META_KEY],
watching = (m && m.watching[keyName] > 0) || keyName === 'length',
proto = m && m.proto,
desc = m && m.descs[keyName];
var m = metaFor(obj, false),
watching = m.watching[keyName] > 0 || keyName === 'length',
proto = m.proto,
desc = m.descs[keyName];
if (proto === obj) { return; }
@ -2642,7 +2642,7 @@ function chainsWillChange(obj, keyName, m) {
}
function chainsDidChange(obj, keyName, m, suppressEvents) {
if (!(m && m.hasOwnProperty('chainWatchers') &&
if (!(m.hasOwnProperty('chainWatchers') &&
m.chainWatchers[keyName])) {
return;
}
@ -3645,11 +3645,11 @@ var metaFor = Ember.meta, // utils.js
MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER,
o_defineProperty = Ember.platform.defineProperty;
Ember.watchKey = function(obj, keyName, meta) {
Ember.watchKey = function(obj, keyName) {
// can't watch length on Array - it is special...
if (keyName === 'length' && typeOf(obj) === 'array') { return; }
var m = meta || metaFor(obj), watching = m.watching;
var m = metaFor(obj), watching = m.watching;
// activate watching first time
if (!watching[keyName]) {
@ -3674,8 +3674,8 @@ Ember.watchKey = function(obj, keyName, meta) {
};
Ember.unwatchKey = function(obj, keyName, meta) {
var m = meta || metaFor(obj), watching = m.watching;
Ember.unwatchKey = function(obj, keyName) {
var m = metaFor(obj), watching = m.watching;
if (watching[keyName] === 1) {
watching[keyName] = 0;
@ -3718,8 +3718,7 @@ var metaFor = Ember.meta, // utils.js
warn = Ember.warn,
watchKey = Ember.watchKey,
unwatchKey = Ember.unwatchKey,
FIRST_KEY = /^([^\.\*]+)/,
META_KEY = Ember.META_KEY;
FIRST_KEY = /^([^\.\*]+)/;
function firstKey(path) {
return path.match(FIRST_KEY)[0];
@ -3753,24 +3752,24 @@ function addChainWatcher(obj, keyName, node) {
if (!nodes[keyName]) { nodes[keyName] = []; }
nodes[keyName].push(node);
watchKey(obj, keyName, m);
watchKey(obj, keyName);
}
var removeChainWatcher = Ember.removeChainWatcher = function(obj, keyName, node) {
if (!obj || 'object' !== typeof obj) { return; } // nothing to do
var m = obj[META_KEY];
if (m && !m.hasOwnProperty('chainWatchers')) { return; } // nothing to do
var m = metaFor(obj, false);
if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do
var nodes = m && m.chainWatchers;
var nodes = m.chainWatchers;
if (nodes && nodes[keyName]) {
if (nodes[keyName]) {
nodes = nodes[keyName];
for (var i = 0, l = nodes.length; i < l; i++) {
if (nodes[i] === node) { nodes.splice(i, 1); }
}
}
unwatchKey(obj, keyName, m);
unwatchKey(obj, keyName);
};
// A ChainNode watches a single key on an object. If you provide a starting
@ -3810,14 +3809,14 @@ var ChainNodePrototype = ChainNode.prototype;
function lazyGet(obj, key) {
if (!obj) return undefined;
var meta = obj[META_KEY];
var meta = metaFor(obj, false);
// check if object meant only to be a prototype
if (meta && meta.proto === obj) return undefined;
if (meta.proto === obj) return undefined;
if (key === "@each") return get(obj, key);
// if a CP only return cached value
var desc = meta && meta.descs[key];
var desc = meta.descs[key];
if (desc && desc._cacheable) {
if (key in meta.cache) {
return meta.cache[key];
@ -4029,14 +4028,12 @@ ChainNodePrototype.didChange = function(events) {
};
Ember.finishChains = function(obj) {
// We only create meta if we really have to
var m = obj[META_KEY], chains = m && m.chains;
var m = metaFor(obj, false), chains = m.chains;
if (chains) {
if (chains.value() !== obj) {
metaFor(obj).chains = chains = chains.copy(obj);
} else {
chains.didChange(null);
m.chains = chains = chains.copy(obj);
}
chains.didChange(null);
}
};
@ -4058,8 +4055,8 @@ var metaFor = Ember.meta, // utils.js
// get the chains for the current object. If the current object has
// chains inherited from the proto they will be cloned and reconfigured for
// the current object.
function chainsFor(obj, meta) {
var m = meta || metaFor(obj), ret = m.chains;
function chainsFor(obj) {
var m = metaFor(obj), ret = m.chains;
if (!ret) {
ret = m.chains = new ChainNode(null, null, obj);
} else if (ret.value() !== obj) {
@ -4068,26 +4065,26 @@ function chainsFor(obj, meta) {
return ret;
}
Ember.watchPath = function(obj, keyPath, meta) {
Ember.watchPath = function(obj, keyPath) {
// can't watch length on Array - it is special...
if (keyPath === 'length' && typeOf(obj) === 'array') { return; }
var m = meta || metaFor(obj), watching = m.watching;
var m = metaFor(obj), watching = m.watching;
if (!watching[keyPath]) { // activate watching first time
watching[keyPath] = 1;
chainsFor(obj, m).add(keyPath);
chainsFor(obj).add(keyPath);
} else {
watching[keyPath] = (watching[keyPath] || 0) + 1;
}
};
Ember.unwatchPath = function(obj, keyPath, meta) {
var m = meta || metaFor(obj), watching = m.watching;
Ember.unwatchPath = function(obj, keyPath) {
var m = metaFor(obj), watching = m.watching;
if (watching[keyPath] === 1) {
watching[keyPath] = 0;
chainsFor(obj, m).remove(keyPath);
chainsFor(obj).remove(keyPath);
} else if (watching[keyPath] > 1) {
watching[keyPath]--;
}
@ -4131,14 +4128,14 @@ function isKeyName(path) {
@param obj
@param {String} keyName
*/
Ember.watch = function(obj, _keyPath, m) {
Ember.watch = function(obj, _keyPath) {
// can't watch length on Array - it is special...
if (_keyPath === 'length' && typeOf(obj) === 'array') { return; }
if (isKeyName(_keyPath)) {
watchKey(obj, _keyPath, m);
watchKey(obj, _keyPath);
} else {
watchPath(obj, _keyPath, m);
watchPath(obj, _keyPath);
}
};
@ -4149,14 +4146,14 @@ Ember.isWatching = function isWatching(obj, key) {
Ember.watch.flushPending = Ember.flushPendingChains;
Ember.unwatch = function(obj, _keyPath, m) {
Ember.unwatch = function(obj, _keyPath) {
// can't watch length on Array - it is special...
if (_keyPath === 'length' && typeOf(obj) === 'array') { return; }
if (isKeyName(_keyPath)) {
unwatchKey(obj, _keyPath, m);
unwatchKey(obj, _keyPath);
} else {
unwatchPath(obj, _keyPath, m);
unwatchPath(obj, _keyPath);
}
};
@ -4171,7 +4168,7 @@ Ember.unwatch = function(obj, _keyPath, m) {
@param obj
*/
Ember.rewatch = function(obj) {
var m = obj[META_KEY], chains = m && m.chains;
var m = metaFor(obj, false), chains = m.chains;
// make sure the object has its own guid.
if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) {
@ -4297,7 +4294,7 @@ function addDependentKeys(desc, obj, keyName, meta) {
// Increment the number of times depKey depends on keyName.
keys[keyName] = (keys[keyName] || 0) + 1;
// Watch the depKey
watch(obj, depKey, meta);
watch(obj, depKey);
}
}
@ -4316,7 +4313,7 @@ function removeDependentKeys(desc, obj, keyName, meta) {
// Increment the number of times depKey depends on keyName.
keys[keyName] = (keys[keyName] || 0) - 1;
// Watch the depKey
unwatch(obj, depKey, meta);
unwatch(obj, depKey);
}
}
@ -4759,8 +4756,7 @@ Ember.computed = function(func) {
@return {Object} the cached value
*/
Ember.cacheFor = function cacheFor(obj, key) {
var meta = obj[META_KEY],
cache = meta && meta.cache;
var cache = metaFor(obj, false).cache;
if (cache && key in cache) {
return cache[key];
@ -7134,13 +7130,11 @@ var Mixin, REQUIRED, Alias,
a_slice = [].slice,
o_create = Ember.create,
defineProperty = Ember.defineProperty,
guidFor = Ember.guidFor,
metaFor = Ember.meta,
META_KEY = Ember.META_KEY;
guidFor = Ember.guidFor;
function mixinsMeta(obj) {
var m = metaFor(obj, true), ret = m.mixins;
var m = Ember.meta(obj, true), ret = m.mixins;
if (!ret) {
ret = m.mixins = {};
} else if (!m.hasOwnProperty('mixins')) {
@ -7325,7 +7319,7 @@ function mergeMixins(mixins, m, descs, values, base, keys) {
if (props === CONTINUE) { continue; }
if (props) {
meta = metaFor(base);
meta = Ember.meta(base);
if (base.willMergeMixin) { base.willMergeMixin(props); }
concats = concatenatedMixinProperties('concatenatedProperties', props, values, base);
mergings = concatenatedMixinProperties('mergedProperties', props, values, base);
@ -7383,7 +7377,7 @@ function connectBindings(obj, m) {
}
function finishPartial(obj, m) {
connectBindings(obj, m || metaFor(obj));
connectBindings(obj, m || Ember.meta(obj));
return obj;
}
@ -7430,7 +7424,7 @@ function replaceObserversAndListeners(obj, key, observerOrListener) {
}
function applyMixin(obj, mixins, partial) {
var descs = {}, values = {}, m = metaFor(obj),
var descs = {}, values = {}, m = Ember.meta(obj),
key, value, desc, keys = [];
// Go through all mixins and hashes passed in, and:
@ -7642,8 +7636,7 @@ function _detect(curMixin, targetMixin, seen) {
MixinPrototype.detect = function(obj) {
if (!obj) { return false; }
if (obj instanceof Mixin) { return _detect(obj, this, {}); }
var m = obj[META_KEY],
mixins = m && m.mixins;
var mixins = Ember.meta(obj, false).mixins;
if (mixins) {
return !!mixins[guidFor(this)];
}
@ -7682,8 +7675,7 @@ MixinPrototype.keys = function() {
// returns the mixins currently applied to the specified object
// TODO: Make Ember.mixin
Mixin.mixins = function(obj) {
var m = obj[META_KEY],
mixins = m && m.mixins, ret = [];
var mixins = Ember.meta(obj, false).mixins, ret = [];
if (!mixins) { return ret; }
@ -9579,7 +9571,7 @@ define("rsvp/promise/all",
```
@method all
@for Ember.RSVP.Promise
@for RSVP.Promise
@param {Array} entries array of promises
@param {String} label optional string for labeling the promise.
Useful for tooling.
@ -12215,7 +12207,6 @@ var set = Ember.set, get = Ember.get,
guidFor = Ember.guidFor,
generateGuid = Ember.generateGuid,
meta = Ember.meta,
META_KEY = Ember.META_KEY,
rewatch = Ember.rewatch,
finishChains = Ember.finishChains,
sendEvent = Ember.sendEvent,
@ -12909,8 +12900,7 @@ var ClassMixin = Mixin.create({
@param key {String} property name
*/
metaForProperty: function(key) {
var meta = this.proto()[META_KEY],
desc = meta && meta.descs[key];
var desc = meta(this.proto(), false).descs[key];
Ember.assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof Ember.ComputedProperty);
return desc._meta || {};
@ -25403,8 +25393,9 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, {
set(this, 'controller', this);
},
defaultLayout: function(context, options){
Ember.Handlebars.helpers['yield'].call(context, options);
defaultLayout: function(options){
options.data = {view: options._context};
Ember.Handlebars.helpers['yield'].apply(this, [options]);
},
// during render, isolate keywords
@ -26510,6 +26501,33 @@ var handlebarsGet = Ember.Handlebars.get = function(root, path, options) {
return value;
};
/**
This method uses `Ember.Handlebars.get` to lookup a value, then ensures
that the value is escaped properly.
If `unescaped` is a truthy value then the escaping will not be performed.
@method getEscaped
@for Ember.Handlebars
@param {Object} root The object to look up the property on
@param {String} path The path to be lookedup
@param {Object} options The template's option hash
*/
Ember.Handlebars.getEscaped = function(root, path, options) {
var result = handlebarsGet(root, path, options);
if (result === null || result === undefined) {
result = "";
} else if (!(result instanceof Handlebars.SafeString)) {
result = String(result);
}
if (!options.hash.unescaped){
result = Handlebars.Utils.escapeExpression(result);
}
return result;
};
Ember.Handlebars.resolveParams = function(context, params, options) {
var resolvedParams = [], types = options.types, param, type;
@ -27467,6 +27485,7 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({
var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt;
var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath;
var handlebarsGetEscaped = Ember.Handlebars.getEscaped;
var forEach = Ember.ArrayPolyfills.forEach;
var o_create = Ember.create;
@ -27476,20 +27495,6 @@ function exists(value) {
return !Ember.isNone(value);
}
function sanitizedHandlebarsGet(currentContext, property, options) {
var result = handlebarsGet(currentContext, property, options);
if (result === null || result === undefined) {
result = "";
} else if (!(result instanceof Handlebars.SafeString)) {
result = String(result);
}
if (!options.hash.unescaped){
result = Handlebars.Utils.escapeExpression(result);
}
return result;
}
// Binds a property into the DOM. This will create a hook in DOM that the
// KVO system will look for and update if the property changes.
function bind(property, options, preserveContext, shouldDisplay, valueNormalizer, childProperties) {
@ -27560,7 +27565,7 @@ function bind(property, options, preserveContext, shouldDisplay, valueNormalizer
} else {
// The object is not observable, so just render it out and
// be done with it.
data.buffer.push(handlebarsGet(currentContext, property, options));
data.buffer.push(handlebarsGetEscaped(currentContext, property, options));
}
}
@ -27581,7 +27586,7 @@ function simpleBind(currentContext, property, options) {
Ember.run.once(view, 'rerender');
};
output = sanitizedHandlebarsGet(currentContext, property, options);
output = handlebarsGetEscaped(currentContext, property, options);
data.buffer.push(output);
} else {
@ -27607,8 +27612,7 @@ function simpleBind(currentContext, property, options) {
} else {
// The object is not observable, so just render it out and
// be done with it.
output = sanitizedHandlebarsGet(currentContext, property, options);
output = handlebarsGetEscaped(currentContext, property, options);
data.buffer.push(output);
}
}
@ -36178,7 +36182,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
if (linkType === 'ID') {
options.linkTextPath = linkTitle;
options.fn = function() {
return Ember.Handlebars.get(context, linkTitle, options);
return Ember.Handlebars.getEscaped(context, linkTitle, options);
};
} else {
options.fn = function() {

View File

@ -5,7 +5,7 @@
* Portions Copyright 2008-2011 Apple Inc. All rights reserved.
* @license Licensed under MIT license
* See https://raw.github.com/emberjs/ember.js/master/LICENSE
* @version 1.3.2+pre.773be0ec
* @version 1.3.2
*/
@ -88,7 +88,7 @@ var define, requireModule, require, requirejs;
@class Ember
@static
@version 1.3.2+pre.773be0ec
@version 1.3.2
*/
if ('undefined' === typeof Ember) {
@ -115,10 +115,10 @@ Ember.toString = function() { return "Ember"; };
/**
@property VERSION
@type String
@default '1.3.2+pre.773be0ec'
@default '1.3.2'
@static
*/
Ember.VERSION = '1.3.2+pre.773be0ec';
Ember.VERSION = '1.3.2';
/**
Standard environmental variables. You can define these in a global `EmberENV`
@ -831,7 +831,7 @@ Ember.guidFor = function guidFor(obj) {
// META
//
var META_DESC = Ember.META_DESC = {
var META_DESC = {
writable: true,
configurable: false,
enumerable: false,
@ -2302,7 +2302,7 @@ ObserverSet.prototype.clear = function() {
(function() {
var META_KEY = Ember.META_KEY,
var metaFor = Ember.meta,
guidFor = Ember.guidFor,
tryFinally = Ember.tryFinally,
sendEvent = Ember.sendEvent,
@ -2333,10 +2333,10 @@ var META_KEY = Ember.META_KEY,
@return {void}
*/
function propertyWillChange(obj, keyName) {
var m = obj[META_KEY],
watching = (m && m.watching[keyName] > 0) || keyName === 'length',
proto = m && m.proto,
desc = m && m.descs[keyName];
var m = metaFor(obj, false),
watching = m.watching[keyName] > 0 || keyName === 'length',
proto = m.proto,
desc = m.descs[keyName];
if (!watching) { return; }
if (proto === obj) { return; }
@ -2363,10 +2363,10 @@ Ember.propertyWillChange = propertyWillChange;
@return {void}
*/
function propertyDidChange(obj, keyName) {
var m = obj[META_KEY],
watching = (m && m.watching[keyName] > 0) || keyName === 'length',
proto = m && m.proto,
desc = m && m.descs[keyName];
var m = metaFor(obj, false),
watching = m.watching[keyName] > 0 || keyName === 'length',
proto = m.proto,
desc = m.descs[keyName];
if (proto === obj) { return; }
@ -2439,7 +2439,7 @@ function chainsWillChange(obj, keyName, m) {
}
function chainsDidChange(obj, keyName, m, suppressEvents) {
if (!(m && m.hasOwnProperty('chainWatchers') &&
if (!(m.hasOwnProperty('chainWatchers') &&
m.chainWatchers[keyName])) {
return;
}
@ -3437,11 +3437,11 @@ var metaFor = Ember.meta, // utils.js
MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER,
o_defineProperty = Ember.platform.defineProperty;
Ember.watchKey = function(obj, keyName, meta) {
Ember.watchKey = function(obj, keyName) {
// can't watch length on Array - it is special...
if (keyName === 'length' && typeOf(obj) === 'array') { return; }
var m = meta || metaFor(obj), watching = m.watching;
var m = metaFor(obj), watching = m.watching;
// activate watching first time
if (!watching[keyName]) {
@ -3466,8 +3466,8 @@ Ember.watchKey = function(obj, keyName, meta) {
};
Ember.unwatchKey = function(obj, keyName, meta) {
var m = meta || metaFor(obj), watching = m.watching;
Ember.unwatchKey = function(obj, keyName) {
var m = metaFor(obj), watching = m.watching;
if (watching[keyName] === 1) {
watching[keyName] = 0;
@ -3510,8 +3510,7 @@ var metaFor = Ember.meta, // utils.js
warn = Ember.warn,
watchKey = Ember.watchKey,
unwatchKey = Ember.unwatchKey,
FIRST_KEY = /^([^\.\*]+)/,
META_KEY = Ember.META_KEY;
FIRST_KEY = /^([^\.\*]+)/;
function firstKey(path) {
return path.match(FIRST_KEY)[0];
@ -3545,24 +3544,24 @@ function addChainWatcher(obj, keyName, node) {
if (!nodes[keyName]) { nodes[keyName] = []; }
nodes[keyName].push(node);
watchKey(obj, keyName, m);
watchKey(obj, keyName);
}
var removeChainWatcher = Ember.removeChainWatcher = function(obj, keyName, node) {
if (!obj || 'object' !== typeof obj) { return; } // nothing to do
var m = obj[META_KEY];
if (m && !m.hasOwnProperty('chainWatchers')) { return; } // nothing to do
var m = metaFor(obj, false);
if (!m.hasOwnProperty('chainWatchers')) { return; } // nothing to do
var nodes = m && m.chainWatchers;
var nodes = m.chainWatchers;
if (nodes && nodes[keyName]) {
if (nodes[keyName]) {
nodes = nodes[keyName];
for (var i = 0, l = nodes.length; i < l; i++) {
if (nodes[i] === node) { nodes.splice(i, 1); }
}
}
unwatchKey(obj, keyName, m);
unwatchKey(obj, keyName);
};
// A ChainNode watches a single key on an object. If you provide a starting
@ -3602,14 +3601,14 @@ var ChainNodePrototype = ChainNode.prototype;
function lazyGet(obj, key) {
if (!obj) return undefined;
var meta = obj[META_KEY];
var meta = metaFor(obj, false);
// check if object meant only to be a prototype
if (meta && meta.proto === obj) return undefined;
if (meta.proto === obj) return undefined;
if (key === "@each") return get(obj, key);
// if a CP only return cached value
var desc = meta && meta.descs[key];
var desc = meta.descs[key];
if (desc && desc._cacheable) {
if (key in meta.cache) {
return meta.cache[key];
@ -3821,14 +3820,12 @@ ChainNodePrototype.didChange = function(events) {
};
Ember.finishChains = function(obj) {
// We only create meta if we really have to
var m = obj[META_KEY], chains = m && m.chains;
var m = metaFor(obj, false), chains = m.chains;
if (chains) {
if (chains.value() !== obj) {
metaFor(obj).chains = chains = chains.copy(obj);
} else {
chains.didChange(null);
m.chains = chains = chains.copy(obj);
}
chains.didChange(null);
}
};
@ -3850,8 +3847,8 @@ var metaFor = Ember.meta, // utils.js
// get the chains for the current object. If the current object has
// chains inherited from the proto they will be cloned and reconfigured for
// the current object.
function chainsFor(obj, meta) {
var m = meta || metaFor(obj), ret = m.chains;
function chainsFor(obj) {
var m = metaFor(obj), ret = m.chains;
if (!ret) {
ret = m.chains = new ChainNode(null, null, obj);
} else if (ret.value() !== obj) {
@ -3860,26 +3857,26 @@ function chainsFor(obj, meta) {
return ret;
}
Ember.watchPath = function(obj, keyPath, meta) {
Ember.watchPath = function(obj, keyPath) {
// can't watch length on Array - it is special...
if (keyPath === 'length' && typeOf(obj) === 'array') { return; }
var m = meta || metaFor(obj), watching = m.watching;
var m = metaFor(obj), watching = m.watching;
if (!watching[keyPath]) { // activate watching first time
watching[keyPath] = 1;
chainsFor(obj, m).add(keyPath);
chainsFor(obj).add(keyPath);
} else {
watching[keyPath] = (watching[keyPath] || 0) + 1;
}
};
Ember.unwatchPath = function(obj, keyPath, meta) {
var m = meta || metaFor(obj), watching = m.watching;
Ember.unwatchPath = function(obj, keyPath) {
var m = metaFor(obj), watching = m.watching;
if (watching[keyPath] === 1) {
watching[keyPath] = 0;
chainsFor(obj, m).remove(keyPath);
chainsFor(obj).remove(keyPath);
} else if (watching[keyPath] > 1) {
watching[keyPath]--;
}
@ -3923,14 +3920,14 @@ function isKeyName(path) {
@param obj
@param {String} keyName
*/
Ember.watch = function(obj, _keyPath, m) {
Ember.watch = function(obj, _keyPath) {
// can't watch length on Array - it is special...
if (_keyPath === 'length' && typeOf(obj) === 'array') { return; }
if (isKeyName(_keyPath)) {
watchKey(obj, _keyPath, m);
watchKey(obj, _keyPath);
} else {
watchPath(obj, _keyPath, m);
watchPath(obj, _keyPath);
}
};
@ -3941,14 +3938,14 @@ Ember.isWatching = function isWatching(obj, key) {
Ember.watch.flushPending = Ember.flushPendingChains;
Ember.unwatch = function(obj, _keyPath, m) {
Ember.unwatch = function(obj, _keyPath) {
// can't watch length on Array - it is special...
if (_keyPath === 'length' && typeOf(obj) === 'array') { return; }
if (isKeyName(_keyPath)) {
unwatchKey(obj, _keyPath, m);
unwatchKey(obj, _keyPath);
} else {
unwatchPath(obj, _keyPath, m);
unwatchPath(obj, _keyPath);
}
};
@ -3963,7 +3960,7 @@ Ember.unwatch = function(obj, _keyPath, m) {
@param obj
*/
Ember.rewatch = function(obj) {
var m = obj[META_KEY], chains = m && m.chains;
var m = metaFor(obj, false), chains = m.chains;
// make sure the object has its own guid.
if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) {
@ -4088,7 +4085,7 @@ function addDependentKeys(desc, obj, keyName, meta) {
// Increment the number of times depKey depends on keyName.
keys[keyName] = (keys[keyName] || 0) + 1;
// Watch the depKey
watch(obj, depKey, meta);
watch(obj, depKey);
}
}
@ -4107,7 +4104,7 @@ function removeDependentKeys(desc, obj, keyName, meta) {
// Increment the number of times depKey depends on keyName.
keys[keyName] = (keys[keyName] || 0) - 1;
// Watch the depKey
unwatch(obj, depKey, meta);
unwatch(obj, depKey);
}
}
@ -4550,8 +4547,7 @@ Ember.computed = function(func) {
@return {Object} the cached value
*/
Ember.cacheFor = function cacheFor(obj, key) {
var meta = obj[META_KEY],
cache = meta && meta.cache;
var cache = metaFor(obj, false).cache;
if (cache && key in cache) {
return cache[key];
@ -6922,13 +6918,11 @@ var Mixin, REQUIRED, Alias,
a_slice = [].slice,
o_create = Ember.create,
defineProperty = Ember.defineProperty,
guidFor = Ember.guidFor,
metaFor = Ember.meta,
META_KEY = Ember.META_KEY;
guidFor = Ember.guidFor;
function mixinsMeta(obj) {
var m = metaFor(obj, true), ret = m.mixins;
var m = Ember.meta(obj, true), ret = m.mixins;
if (!ret) {
ret = m.mixins = {};
} else if (!m.hasOwnProperty('mixins')) {
@ -7111,7 +7105,7 @@ function mergeMixins(mixins, m, descs, values, base, keys) {
if (props === CONTINUE) { continue; }
if (props) {
meta = metaFor(base);
meta = Ember.meta(base);
if (base.willMergeMixin) { base.willMergeMixin(props); }
concats = concatenatedMixinProperties('concatenatedProperties', props, values, base);
mergings = concatenatedMixinProperties('mergedProperties', props, values, base);
@ -7169,7 +7163,7 @@ function connectBindings(obj, m) {
}
function finishPartial(obj, m) {
connectBindings(obj, m || metaFor(obj));
connectBindings(obj, m || Ember.meta(obj));
return obj;
}
@ -7216,7 +7210,7 @@ function replaceObserversAndListeners(obj, key, observerOrListener) {
}
function applyMixin(obj, mixins, partial) {
var descs = {}, values = {}, m = metaFor(obj),
var descs = {}, values = {}, m = Ember.meta(obj),
key, value, desc, keys = [];
// Go through all mixins and hashes passed in, and:
@ -7426,8 +7420,7 @@ function _detect(curMixin, targetMixin, seen) {
MixinPrototype.detect = function(obj) {
if (!obj) { return false; }
if (obj instanceof Mixin) { return _detect(obj, this, {}); }
var m = obj[META_KEY],
mixins = m && m.mixins;
var mixins = Ember.meta(obj, false).mixins;
if (mixins) {
return !!mixins[guidFor(this)];
}
@ -7466,8 +7459,7 @@ MixinPrototype.keys = function() {
// returns the mixins currently applied to the specified object
// TODO: Make Ember.mixin
Mixin.mixins = function(obj) {
var m = obj[META_KEY],
mixins = m && m.mixins, ret = [];
var mixins = Ember.meta(obj, false).mixins, ret = [];
if (!mixins) { return ret; }
@ -9361,7 +9353,7 @@ define("rsvp/promise/all",
```
@method all
@for Ember.RSVP.Promise
@for RSVP.Promise
@param {Array} entries array of promises
@param {String} label optional string for labeling the promise.
Useful for tooling.
@ -11994,7 +11986,6 @@ var set = Ember.set, get = Ember.get,
guidFor = Ember.guidFor,
generateGuid = Ember.generateGuid,
meta = Ember.meta,
META_KEY = Ember.META_KEY,
rewatch = Ember.rewatch,
finishChains = Ember.finishChains,
sendEvent = Ember.sendEvent,
@ -12682,8 +12673,7 @@ var ClassMixin = Mixin.create({
@param key {String} property name
*/
metaForProperty: function(key) {
var meta = this.proto()[META_KEY],
desc = meta && meta.descs[key];
var desc = meta(this.proto(), false).descs[key];
return desc._meta || {};
},
@ -25122,8 +25112,9 @@ Ember.Component = Ember.View.extend(Ember.TargetActionSupport, {
set(this, 'controller', this);
},
defaultLayout: function(context, options){
Ember.Handlebars.helpers['yield'].call(context, options);
defaultLayout: function(options){
options.data = {view: options._context};
Ember.Handlebars.helpers['yield'].apply(this, [options]);
},
// during render, isolate keywords
@ -26212,6 +26203,33 @@ var handlebarsGet = Ember.Handlebars.get = function(root, path, options) {
return value;
};
/**
This method uses `Ember.Handlebars.get` to lookup a value, then ensures
that the value is escaped properly.
If `unescaped` is a truthy value then the escaping will not be performed.
@method getEscaped
@for Ember.Handlebars
@param {Object} root The object to look up the property on
@param {String} path The path to be lookedup
@param {Object} options The template's option hash
*/
Ember.Handlebars.getEscaped = function(root, path, options) {
var result = handlebarsGet(root, path, options);
if (result === null || result === undefined) {
result = "";
} else if (!(result instanceof Handlebars.SafeString)) {
result = String(result);
}
if (!options.hash.unescaped){
result = Handlebars.Utils.escapeExpression(result);
}
return result;
};
Ember.Handlebars.resolveParams = function(context, params, options) {
var resolvedParams = [], types = options.types, param, type;
@ -27162,6 +27180,7 @@ Ember._HandlebarsBoundView = Ember._MetamorphView.extend({
var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt;
var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath;
var handlebarsGetEscaped = Ember.Handlebars.getEscaped;
var forEach = Ember.ArrayPolyfills.forEach;
var o_create = Ember.create;
@ -27171,20 +27190,6 @@ function exists(value) {
return !Ember.isNone(value);
}
function sanitizedHandlebarsGet(currentContext, property, options) {
var result = handlebarsGet(currentContext, property, options);
if (result === null || result === undefined) {
result = "";
} else if (!(result instanceof Handlebars.SafeString)) {
result = String(result);
}
if (!options.hash.unescaped){
result = Handlebars.Utils.escapeExpression(result);
}
return result;
}
// Binds a property into the DOM. This will create a hook in DOM that the
// KVO system will look for and update if the property changes.
function bind(property, options, preserveContext, shouldDisplay, valueNormalizer, childProperties) {
@ -27255,7 +27260,7 @@ function bind(property, options, preserveContext, shouldDisplay, valueNormalizer
} else {
// The object is not observable, so just render it out and
// be done with it.
data.buffer.push(handlebarsGet(currentContext, property, options));
data.buffer.push(handlebarsGetEscaped(currentContext, property, options));
}
}
@ -27276,7 +27281,7 @@ function simpleBind(currentContext, property, options) {
Ember.run.once(view, 'rerender');
};
output = sanitizedHandlebarsGet(currentContext, property, options);
output = handlebarsGetEscaped(currentContext, property, options);
data.buffer.push(output);
} else {
@ -27302,8 +27307,7 @@ function simpleBind(currentContext, property, options) {
} else {
// The object is not observable, so just render it out and
// be done with it.
output = sanitizedHandlebarsGet(currentContext, property, options);
output = handlebarsGetEscaped(currentContext, property, options);
data.buffer.push(output);
}
}
@ -35799,7 +35803,7 @@ Ember.onLoad('Ember.Handlebars', function(Handlebars) {
if (linkType === 'ID') {
options.linkTextPath = linkTitle;
options.fn = function() {
return Ember.Handlebars.get(context, linkTitle, options);
return Ember.Handlebars.getEscaped(context, linkTitle, options);
};
} else {
options.fn = function() {