Merge branch 'master' into fixed_modals

Conflicts:
	app/assets/javascripts/discourse/templates/modal/modal.js.handlebars
	app/assets/stylesheets/application/modal.css.scss
This commit is contained in:
Brentley Jones 2013-07-16 18:58:49 +00:00
commit 5e08427dd3
99 changed files with 6693 additions and 2361 deletions

1
.gitignore vendored
View File

@ -25,6 +25,7 @@ dump.rdb
config/database.yml config/database.yml
config/redis.yml config/redis.yml
config/discourse.pill config/discourse.pill
config/environments/production.rb
# Ignore the default SQLite database and db dumps # Ignore the default SQLite database and db dumps
/db/*.sqlite3 /db/*.sqlite3

View File

@ -5,8 +5,8 @@ gem 'active_model_serializers', git: 'https://github.com/rails-api/active_model_
# we had issues with latest, stick to the rev till we figure this out # we had issues with latest, stick to the rev till we figure this out
# PR that makes it all hang together welcome # PR that makes it all hang together welcome
gem 'ember-rails' gem 'ember-rails'
gem 'ember-source', '1.0.0.rc5' # or the version you need gem 'ember-source', '1.0.0.rc6.2'
gem 'handlebars-source', '1.0.0.rc4' # or the version you need gem 'handlebars-source', '1.0.12'
gem 'barber' gem 'barber'
gem 'vestal_versions', git: 'https://github.com/zhangyuan/vestal_versions' gem 'vestal_versions', git: 'https://github.com/zhangyuan/vestal_versions'

View File

@ -169,8 +169,8 @@ GEM
barber barber
execjs (>= 1.2) execjs (>= 1.2)
railties (>= 3.1) railties (>= 3.1)
ember-source (1.0.0.rc5) ember-source (1.0.0.rc6.2)
handlebars-source (= 1.0.0.rc4) handlebars-source (= 1.0.12)
erubis (2.7.0) erubis (2.7.0)
eventmachine (1.0.3) eventmachine (1.0.3)
excon (0.20.1) excon (0.20.1)
@ -211,7 +211,7 @@ GEM
childprocess (>= 0.2.3) childprocess (>= 0.2.3)
guard (>= 1.1) guard (>= 1.1)
spork (>= 0.8.4) spork (>= 0.8.4)
handlebars-source (1.0.0.rc4) handlebars-source (1.0.12)
hashie (2.0.4) hashie (2.0.4)
highline (1.6.18) highline (1.6.18)
hike (1.2.2) hike (1.2.2)
@ -482,7 +482,7 @@ DEPENDENCIES
em-redis em-redis
email_reply_parser! email_reply_parser!
ember-rails ember-rails
ember-source (= 1.0.0.rc5) ember-source (= 1.0.0.rc6.2)
eventmachine eventmachine
fabrication fabrication
fakeweb (~> 1.3.0) fakeweb (~> 1.3.0)
@ -493,7 +493,7 @@ DEPENDENCIES
fog fog
guard-rspec guard-rspec
guard-spork guard-spork
handlebars-source (= 1.0.0.rc4) handlebars-source (= 1.0.12)
highline highline
hiredis hiredis
image_optim image_optim

View File

@ -238,9 +238,9 @@ Discourse.AdminUser = Discourse.User.extend({
loadDetails: function() { loadDetails: function() {
var model = this; var model = this;
if (model.get('loadedDetails')) { return; } if (model.get('loadedDetails')) { return Ember.RSVP.resolve(model); }
Discourse.AdminUser.find(model.get('username_lower')).then(function (result) { return Discourse.AdminUser.find(model.get('username_lower')).then(function (result) {
model.setProperties(result); model.setProperties(result);
model.set('loadedDetails', true); model.set('loadedDetails', true);
}); });

View File

@ -11,14 +11,16 @@ var oneWeekAgo = function() {
return moment().subtract('days',7).format('YYYY-MM-DD'); return moment().subtract('days',7).format('YYYY-MM-DD');
}; };
Discourse.AdminEmailPreviewDigestRoute = Discourse.Route.extend(Discourse.ModelReady, { Discourse.AdminEmailPreviewDigestRoute = Discourse.Route.extend({
model: function() { model: function() {
return Discourse.EmailPreview.findDigest(oneWeekAgo()); return Discourse.EmailPreview.findDigest(oneWeekAgo());
}, },
modelReady: function(controller, model) { afterModel: function(model) {
var controller = this.controllerFor('adminEmailPreviewDigest');
controller.setProperties({ controller.setProperties({
model: model,
lastSeen: oneWeekAgo(), lastSeen: oneWeekAgo(),
showHtml: true showHtml: true
}); });

View File

@ -6,7 +6,7 @@
@namespace Discourse @namespace Discourse
@module Discourse @module Discourse
**/ **/
Discourse.AdminUserRoute = Discourse.Route.extend(Discourse.ModelReady, { Discourse.AdminUserRoute = Discourse.Route.extend({
serialize: function(params) { serialize: function(params) {
return { username: Em.get(params, 'username').toLowerCase() }; return { username: Em.get(params, 'username').toLowerCase() };
@ -20,10 +20,14 @@ Discourse.AdminUserRoute = Discourse.Route.extend(Discourse.ModelReady, {
this.render({into: 'admin/templates/admin'}); this.render({into: 'admin/templates/admin'});
}, },
modelReady: function(controller, adminUser) { afterModel: function(adminUser) {
adminUser.loadDetails(); var controller = this.controllerFor('adminUser');
controller.set('model', adminUser);
adminUser.setOriginalTrustLevel(); adminUser.loadDetails().then(function () {
adminUser.setOriginalTrustLevel();
controller.set('model', adminUser);
});
} }
}); });

View File

@ -1,9 +1,9 @@
<div class='admin-controls'> <div class='admin-controls'>
<div class='span15'> <div class='span15'>
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li>{{#linkTo adminEmail.index}}{{i18n admin.email.settings}}{{/linkTo}}</li> <li>{{#linkTo 'adminEmail.index'}}{{i18n admin.email.settings}}{{/linkTo}}</li>
<li>{{#linkTo adminEmail.logs}}{{i18n admin.email.logs}}{{/linkTo}}</li> <li>{{#linkTo 'adminEmail.logs'}}{{i18n admin.email.logs}}{{/linkTo}}</li>
<li>{{#linkTo adminEmail.previewDigest}}{{i18n admin.email.preview_digest}}{{/linkTo}}</li> <li>{{#linkTo 'adminEmail.previewDigest'}}{{i18n admin.email.preview_digest}}{{/linkTo}}</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -1,8 +1,8 @@
<div class='admin-controls'> <div class='admin-controls'>
<div class='span15'> <div class='span15'>
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li>{{#linkTo adminFlags.active}}{{i18n admin.flags.active}}{{/linkTo}}</li> <li>{{#linkTo 'adminFlags.active'}}{{i18n admin.flags.active}}{{/linkTo}}</li>
<li>{{#linkTo adminFlags.old}}{{i18n admin.flags.old}}{{/linkTo}}</li> <li>{{#linkTo 'adminFlags.old'}}{{i18n admin.flags.old}}{{/linkTo}}</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -1,15 +1,15 @@
<div class='admin-controls'> <div class='admin-controls'>
<div class='span15'> <div class='span15'>
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li>{{#linkTo adminUsersList.active}}{{i18n admin.users.nav.active}}{{/linkTo}}</li> <li>{{#linkTo 'adminUsersList.active'}}{{i18n admin.users.nav.active}}{{/linkTo}}</li>
<li>{{#linkTo adminUsersList.new}}{{i18n admin.users.nav.new}}{{/linkTo}}</li> <li>{{#linkTo 'adminUsersList.new'}}{{i18n admin.users.nav.new}}{{/linkTo}}</li>
{{#if Discourse.SiteSettings.must_approve_users}} {{#if Discourse.SiteSettings.must_approve_users}}
<li>{{#linkTo adminUsersList.pending}}{{i18n admin.users.nav.pending}}{{/linkTo}}</li> <li>{{#linkTo 'adminUsersList.pending'}}{{i18n admin.users.nav.pending}}{{/linkTo}}</li>
{{/if}} {{/if}}
<li>{{#linkTo adminUsersList.admins}}{{i18n admin.users.nav.admins}}{{/linkTo}}</li> <li>{{#linkTo 'adminUsersList.admins'}}{{i18n admin.users.nav.admins}}{{/linkTo}}</li>
<li>{{#linkTo adminUsersList.moderators}}{{i18n admin.users.nav.moderators}}{{/linkTo}}</li> <li>{{#linkTo 'adminUsersList.moderators'}}{{i18n admin.users.nav.moderators}}{{/linkTo}}</li>
<li>{{#linkTo adminUsersList.banned}}{{i18n admin.users.nav.banned}}{{/linkTo}}</li> <li>{{#linkTo 'adminUsersList.banned'}}{{i18n admin.users.nav.banned}}{{/linkTo}}</li>
<li>{{#linkTo adminUsersList.blocked}}{{i18n admin.users.nav.blocked}}{{/linkTo}}</li> <li>{{#linkTo 'adminUsersList.blocked'}}{{i18n admin.users.nav.blocked}}{{/linkTo}}</li>
</ul> </ul>
</div> </div>
<div class='span5 username controls'> <div class='span5 username controls'>

View File

@ -12,7 +12,7 @@
// Externals we need to load first // Externals we need to load first
//= require ./external/jquery-1.9.1.js //= require ./external/jquery-1.9.1.js
//= require ./external/jquery.ui.widget.js //= require ./external/jquery.ui.widget.js
//= require ./external/handlebars-1.0.rc.4.js //= require ./external/handlebars.js
<% <%
if Rails.env.development? if Rails.env.development?
require_asset ("./external_development/ember.js") require_asset ("./external_development/ember.js")

View File

@ -250,9 +250,7 @@ Discourse = Ember.Application.createWithMixins({
// If we have URL_FIXTURES, load from there instead (testing) // If we have URL_FIXTURES, load from there instead (testing)
var fixture = Discourse.URL_FIXTURES && Discourse.URL_FIXTURES[url]; var fixture = Discourse.URL_FIXTURES && Discourse.URL_FIXTURES[url];
if (fixture) { if (fixture) {
return Ember.Deferred.promise(function(promise) { return Ember.RSVP.resolve(fixture);
promise.resolve(fixture);
});
} }
return Ember.Deferred.promise(function (promise) { return Ember.Deferred.promise(function (promise) {

View File

@ -254,7 +254,7 @@ Discourse.BBCode = {
// Arguments for formatting // Arguments for formatting
args = { args = {
username: username, username: I18n.t('user.said',{username: username}),
params: params, params: params,
quote: content, quote: content,
avatarImg: opts.lookupAvatar ? opts.lookupAvatar(username) : void 0 avatarImg: opts.lookupAvatar ? opts.lookupAvatar(username) : void 0

View File

@ -101,14 +101,12 @@ Discourse.EditCategoryController = Discourse.ObjectController.extend(Discourse.M
return false; return false;
}, },
addGroup: function(){ addPermission: function(group, permission_id){
this.get('model').addGroup(this.get("selectedGroup")); this.get('model').addPermission({group_name: group + "", permission: Discourse.PermissionType.create({id: permission_id})});
}, },
removeGroup: function(group){ removePermission: function(permission){
// OBVIOUS, Ember treats this as Ember.String, we need a real string here this.get('model').removePermission(permission);
group = group + "";
this.get('model').removeGroup(group);
}, },
saveCategory: function() { saveCategory: function() {

View File

@ -100,11 +100,16 @@ Discourse.QuoteButtonController = Discourse.Controller.extend({
var post = this.get('post'); var post = this.get('post');
var composerController = this.get('controllers.composer'); var composerController = this.get('controllers.composer');
var composerOpts = { var composerOpts = {
post: post,
action: Discourse.Composer.REPLY, action: Discourse.Composer.REPLY,
draftKey: this.get('post.topic.draft_key') draftKey: this.get('post.topic.draft_key')
}; };
if(post.get('post_number') === 1) {
composerOpts.topic = post.get("topic");
} else {
composerOpts.post = post;
}
// If the composer is associated with a different post, we don't change it. // If the composer is associated with a different post, we don't change it.
var composerPost = composerController.get('content.post'); var composerPost = composerController.get('content.post');
if (composerPost && (composerPost.get('id') !== this.get('post.id'))) { if (composerPost && (composerPost.get('id') !== this.get('post.id'))) {

View File

@ -20,7 +20,7 @@ Discourse.StaticController = Discourse.Controller.extend({
text = text[1]; text = text[1];
this.set('content', text); this.set('content', text);
} else { } else {
return Discourse.ajax(path).then(function (result) { return Discourse.ajax(path, {dataType: 'html'}).then(function (result) {
staticController.set('content', result); staticController.set('content', result);
}); });
} }

View File

@ -198,41 +198,6 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
Discourse.URL.routeTo(this.get('lastPostUrl')); Discourse.URL.routeTo(this.get('lastPostUrl'));
}, },
replyAsNewTopic: function(post) {
// TODO shut down topic draft cleanly if it exists ...
var composerController = this.get('controllers.composer');
var promise = composerController.open({
action: Discourse.Composer.CREATE_TOPIC,
draftKey: Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY
});
var postUrl = "" + location.protocol + "//" + location.host + (post.get('url'));
var postLink = "[" + (this.get('title')) + "](" + postUrl + ")";
promise.then(function() {
Discourse.Post.loadQuote(post.get('id')).then(function(q) {
composerController.appendText("" + (I18n.t("post.continue_discussion", {
postLink: postLink
})) + "\n\n" + q);
});
});
},
// Topic related
reply: function() {
var composerController = this.get('controllers.composer');
if (composerController.get('content.topic.id') === this.get('content.id') &&
composerController.get('content.action') === Discourse.Composer.REPLY) {
composerController.set('content.post', null);
composerController.set('content.composeState', Discourse.Composer.OPEN);
} else {
composerController.open({
topic: this.get('content'),
action: Discourse.Composer.REPLY,
draftKey: this.get('content.draft_key'),
draftSequence: this.get('content.draft_sequence')
});
}
},
/** /**
Toggle a participant for filtering Toggle a participant for filtering
@ -336,25 +301,60 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
var composerController = this.get('controllers.composer'); var composerController = this.get('controllers.composer');
var quoteController = this.get('controllers.quoteButton'); var quoteController = this.get('controllers.quoteButton');
var quotedText = Discourse.BBCode.buildQuoteBBCode(quoteController.get('post'), quoteController.get('buffer')); var quotedText = Discourse.BBCode.buildQuoteBBCode(quoteController.get('post'), quoteController.get('buffer'));
var topic = post ? post.get('topic') : this.get('model');
quoteController.set('buffer', ''); quoteController.set('buffer', '');
if (composerController.get('content.topic.id') === post.get('topic.id') && if (composerController.get('content.topic.id') === topic.get('id') &&
composerController.get('content.action') === Discourse.Composer.REPLY) { composerController.get('content.action') === Discourse.Composer.REPLY) {
composerController.set('content.post', post); composerController.set('content.post', post);
composerController.set('content.composeState', Discourse.Composer.OPEN); composerController.set('content.composeState', Discourse.Composer.OPEN);
composerController.appendText(quotedText); composerController.appendText(quotedText);
} else { } else {
var promise = composerController.open({
post: post, var opts = {
action: Discourse.Composer.REPLY, action: Discourse.Composer.REPLY,
draftKey: post.get('topic.draft_key'), draftKey: topic.get('draft_key'),
draftSequence: post.get('topic.draft_sequence') draftSequence: topic.get('draft_sequence')
}); };
if(post && post.get("post_number") !== 1){
opts.post = post;
} else {
opts.topic = topic;
}
var promise = composerController.open(opts);
promise.then(function() { composerController.appendText(quotedText); }); promise.then(function() { composerController.appendText(quotedText); });
} }
return false; return false;
}, },
replyAsNewTopic: function(post) {
// TODO shut down topic draft cleanly if it exists ...
var composerController = this.get('controllers.composer');
var promise = composerController.open({
action: Discourse.Composer.CREATE_TOPIC,
draftKey: Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY
});
var postUrl = "" + location.protocol + "//" + location.host + (post.get('url'));
var postLink = "[" + (this.get('title')) + "](" + postUrl + ")";
promise.then(function() {
Discourse.Post.loadQuote(post.get('id')).then(function(q) {
composerController.appendText("" + (I18n.t("post.continue_discussion", {
postLink: postLink
})) + "\n\n" + q);
});
});
},
// Topic related
reply: function() {
this.replyToPost();
},
// Edits a post // Edits a post
editPost: function(post) { editPost: function(post) {
this.get('controllers.composer').open({ this.get('controllers.composer').open({

View File

@ -1,29 +0,0 @@
/**
Until the fully async router is merged into Ember, it is healthy to do some extra checking
that setupController is not passed a promise instead of the model we want.
This mixin handles that case, and calls modelReady instead.
@class Discourse.ModelReady
@extends Ember.Mixin
@namespace Discourse
@module Discourse
**/
Discourse.ModelReady = Em.Mixin.create({
setupController: function(controller, model) {
var route = this;
if (model.then) {
model.then(function (m) {
controller.set('model', m);
if (route.modelReady) { route.modelReady(controller, m); }
});
} else {
controller.set('model', model);
if (route.modelReady) { route.modelReady(controller, model); }
}
}
});

View File

@ -11,9 +11,21 @@ Discourse.Category = Discourse.Model.extend({
init: function() { init: function() {
this._super(); this._super();
this.set("availableGroups", Em.A(this.get("available_groups"))); this.set("availableGroups", Em.A(this.get("available_groups")));
this.set("groups", Em.A(this.groups)); this.set("permissions", Em.A(_.map(this.group_permissions, function(elem){
return {
group_name: elem.group_name,
permission: Discourse.PermissionType.create({id: elem.permission_type})
};
})));
}, },
availablePermissions: function(){
return [ Discourse.PermissionType.create({id: Discourse.PermissionType.FULL}),
Discourse.PermissionType.create({id: Discourse.PermissionType.CREATE_POST}),
Discourse.PermissionType.create({id: Discourse.PermissionType.READONLY})
];
}.property(),
searchContext: function() { searchContext: function() {
return ({ type: 'category', id: this.get('id'), category: this }); return ({ type: 'category', id: this.get('id'), category: this });
}.property('id'), }.property('id'),
@ -43,33 +55,49 @@ Discourse.Category = Discourse.Model.extend({
text_color: this.get('text_color'), text_color: this.get('text_color'),
hotness: this.get('hotness'), hotness: this.get('hotness'),
secure: this.get('secure'), secure: this.get('secure'),
group_names: this.get('groups').join(","), permissions: this.get('permissionsForUpdate'),
auto_close_days: this.get('auto_close_days') auto_close_days: this.get('auto_close_days')
}, },
type: this.get('id') ? 'PUT' : 'POST' type: this.get('id') ? 'PUT' : 'POST'
}); });
}, },
permissionsForUpdate: function(){
var rval = {};
_.each(this.get("permissions"),function(p){
rval[p.group_name] = p.permission.id;
});
return rval;
}.property("permissions"),
destroy: function(callback) { destroy: function(callback) {
return Discourse.ajax("/categories/" + (this.get('slug') || this.get('id')), { type: 'DELETE' }); return Discourse.ajax("/categories/" + (this.get('slug') || this.get('id')), { type: 'DELETE' });
}, },
addGroup: function(group){ addPermission: function(permission){
this.get("groups").addObject(group); this.get("permissions").addObject(permission);
this.get("availableGroups").removeObject(group); this.get("availableGroups").removeObject(permission.group_name);
}, },
removeGroup: function(group){ removePermission: function(permission){
this.get("groups").removeObject(group); this.get("permissions").removeObject(permission);
this.get("availableGroups").addObject(group); this.get("availableGroups").addObject(permission.group_name);
}, },
// note, this is used in a data attribute, data attributes get downcased // note, this is used in a data attribute, data attributes get downcased
// to avoid confusion later on using this naming here. // to avoid confusion later on using this naming here.
description_text: function(){ description_text: function(){
return $("<div>" + this.get("description") + "</div>").text(); return $("<div>" + this.get("description") + "</div>").text();
}.property("description") }.property("description"),
permissions: function(){
return Em.A([
{group_name: "everyone", permission: Discourse.PermissionType.create({id: 1})},
{group_name: "admins", permission: Discourse.PermissionType.create({id: 2}) },
{group_name: "crap", permission: Discourse.PermissionType.create({id: 3}) }
]);
}.property()
}); });

View File

@ -0,0 +1,23 @@
Discourse.PermissionType = Discourse.Model.extend({
description: function(){
var key = "";
switch(this.get("id")){
case 1:
key = "full";
break;
case 2:
key = "create_post";
break;
case 3:
key = "readonly";
break;
}
return I18n.t("permission_types." + key);
}.property("id")
});
Discourse.PermissionType.FULL = 1;
Discourse.PermissionType.CREATE_POST = 2;
Discourse.PermissionType.READONLY = 3;

View File

@ -9,9 +9,14 @@
Discourse.Post = Discourse.Model.extend({ Discourse.Post = Discourse.Model.extend({
shareUrl: function() { shareUrl: function() {
if (this.get('firstPost')) return this.get('topic.url');
var user = Discourse.User.current(); var user = Discourse.User.current();
return this.get('url') + (user ? '?u=' + user.get('username_lower') : ''); var userSuffix = user ? '?u=' + user.get('username_lower') : '';
if (this.get('firstPost')) {
return this.get('topic.url') + userSuffix;
} else {
return this.get('url') + userSuffix ;
}
}.property('url'), }.property('url'),
new_user: Em.computed.equal('trust_level', 0), new_user: Em.computed.equal('trust_level', 0),

View File

@ -8,7 +8,7 @@
**/ **/
Discourse.RestrictedUserRoute = Discourse.Route.extend({ Discourse.RestrictedUserRoute = Discourse.Route.extend({
redirect: function() { afterModel: function() {
var user = this.modelFor('user'); var user = this.modelFor('user');
if (!user.get('can_edit')) { if (!user.get('can_edit')) {
this.transitionTo('user.activity', user); this.transitionTo('user.activity', user);

View File

@ -6,13 +6,16 @@
@namespace Discourse @namespace Discourse
@module Discourse @module Discourse
**/ **/
Discourse.ListCategoriesRoute = Discourse.Route.extend(Discourse.ModelReady, { Discourse.ListCategoriesRoute = Discourse.Route.extend({
redirect: function() { Discourse.redirectIfLoginRequired(this); }, redirect: function() { Discourse.redirectIfLoginRequired(this); },
events: { events: {
createCategory: function() { createCategory: function() {
Discourse.Route.showModal(this, 'editCategory', Discourse.Category.create({ color: 'AB9364', text_color: 'FFFFFF', hotness: 5 })); Discourse.Route.showModal(this, 'editCategory', Discourse.Category.create({
color: 'AB9364', text_color: 'FFFFFF', hotness: 5, group_permissions: [{group_name: "everyone", permission_type: 1}],
available_groups: Discourse.Site.instance().group_names
}));
this.controllerFor('editCategory').set('selectedTab', 'general'); this.controllerFor('editCategory').set('selectedTab', 'general');
} }
}, },
@ -28,9 +31,11 @@ Discourse.ListCategoriesRoute = Discourse.Route.extend(Discourse.ModelReady, {
this.controllerFor('list').set('canCreateCategory', false); this.controllerFor('list').set('canCreateCategory', false);
}, },
modelReady: function(controller, categoryList) { renderTemplate: function() {
this.render('listCategories', { into: 'list', outlet: 'listView' }); this.render('listCategories', { into: 'list', outlet: 'listView' });
},
afterModel: function(categoryList) {
this.controllerFor('list').setProperties({ this.controllerFor('list').setProperties({
canCreateCategory: categoryList.get('can_create_category'), canCreateCategory: categoryList.get('can_create_category'),
canCreateTopic: categoryList.get('can_create_topic'), canCreateTopic: categoryList.get('can_create_topic'),

View File

@ -9,9 +9,11 @@
Discourse.StaticController.pages.forEach(function(page) { Discourse.StaticController.pages.forEach(function(page) {
Discourse[(page.capitalize()) + "Route"] = Discourse.Route.extend({ Discourse[(page.capitalize()) + "Route"] = Discourse.Route.extend({
renderTemplate: function() { renderTemplate: function() {
this.render('static'); this.render('static');
}, },
setupController: function() { setupController: function() {
var config_key = Discourse.StaticController.configs[page]; var config_key = Discourse.StaticController.configs[page];
if (config_key && Discourse.SiteSettings[config_key].length > 0) { if (config_key && Discourse.SiteSettings[config_key].length > 0) {
@ -20,6 +22,7 @@ Discourse.StaticController.pages.forEach(function(page) {
this.controllerFor('static').loadPath("/" + page); this.controllerFor('static').loadPath("/" + page);
} }
} }
}); });
}); });

View File

@ -6,7 +6,7 @@
@namespace Discourse @namespace Discourse
@module Discourse @module Discourse
**/ **/
Discourse.UserInvitedRoute = Discourse.Route.extend(Discourse.ModelReady, { Discourse.UserInvitedRoute = Discourse.Route.extend({
renderTemplate: function() { renderTemplate: function() {
this.render({ into: 'user', outlet: 'userOutlet' }); this.render({ into: 'user', outlet: 'userOutlet' });

View File

@ -85,7 +85,7 @@
</li> </li>
<li class='current-user'> <li class='current-user'>
{{#if currentUser}} {{#if currentUser}}
{{#titledLinkTo user.activity currentUser titleKey="current_user" class="icon"}}{{avatar currentUser imageSize="medium" }}{{/titledLinkTo}} {{#titledLinkTo 'user.activity' currentUser titleKey="current_user" class="icon"}}{{avatar currentUser imageSize="medium" }}{{/titledLinkTo}}
{{else}} {{else}}
<div class="icon not-logged-in-avatar" {{action showLogin}}><i class='icon-user'></i></div> <div class="icon not-logged-in-avatar" {{action showLogin}}><i class='icon-user'></i></div>
{{/if}} {{/if}}
@ -110,7 +110,7 @@
</section> </section>
<section class='d-dropdown' id='site-map-dropdown'> <section class='d-dropdown' id='site-map-dropdown'>
<ul> <ul class="location-links">
{{#if currentUser.staff}} {{#if currentUser.staff}}
<li><a href="/admin"><i class='icon icon-wrench'></i>{{i18n admin_title}}</a></li> <li><a href="/admin"><i class='icon icon-wrench'></i>{{i18n admin_title}}</a></li>
<li><a href="/admin/flags/active"><i class='icon icon-flag'></i>{{i18n flags_title}}</a> <li><a href="/admin/flags/active"><i class='icon icon-flag'></i>{{i18n flags_title}}</a>
@ -123,8 +123,10 @@
{{#titledLinkTo "list.latest" titleKey="filters.latest.help"}}{{i18n filters.latest.title}}{{/titledLinkTo}} {{#titledLinkTo "list.latest" titleKey="filters.latest.help"}}{{i18n filters.latest.title}}{{/titledLinkTo}}
</li> </li>
<li>{{faqLink}}</li> <li>{{faqLink}}</li>
</ul>
{{#if categories}} {{#if categories}}
<ul class="category-links">
<li class='heading' title="{{i18n filters.categories.help}}"> <li class='heading' title="{{i18n filters.categories.help}}">
{{#linkTo "list.categories"}}{{i18n filters.categories.title}}{{/linkTo}} {{#linkTo "list.categories"}}{{i18n filters.categories.title}}{{/linkTo}}
</li> </li>
@ -135,9 +137,9 @@
<b>{{unbound topic_count}}</b></a> <b>{{unbound topic_count}}</b></a>
</li> </li>
{{/each}} {{/each}}
{{/if}} </ul>
{{/if}}
</ul>
</section> </section>
</div> </div>

View File

@ -62,7 +62,7 @@
<a href='#' {{action createTopic}}>{{i18n topic.suggest_create_topic}}</a> <a href='#' {{action createTopic}}>{{i18n topic.suggest_create_topic}}</a>
{{/if}} {{/if}}
{{else}} {{else}}
{{#linkTo list.categories}}{{i18n topic.browse_all_categories}}{{/linkTo}} {{i18n or}} {{#linkTo list.latest}}{{i18n topic.view_latest_topics}}{{/linkTo}} {{#linkTo 'list.categories'}}{{i18n topic.browse_all_categories}}{{/linkTo}} {{i18n or}} {{#linkTo 'list.latest'}}{{i18n topic.view_latest_topics}}{{/linkTo}}
{{/if}} {{/if}}
{{/if}} {{/if}}
</h3> </h3>

View File

@ -60,25 +60,18 @@
{{#unless isUncategorized}} {{#unless isUncategorized}}
<div {{bindAttr class=":modal-tab :options-tab securitySelected::invisible"}}> <div {{bindAttr class=":modal-tab :options-tab securitySelected::invisible"}}>
<section class='field'> <section class='field'>
<label> <ul class='permission-list'>
{{input type="checkbox" checked=secure}} {{#each permissions}}
{{i18n category.is_secure}} <li>
</label> <span class="name"><span class="badge-group">{{group_name}}</span></span>
{{#if secure}} <span class="permission">{{permission.description}}</span>
<div class="secure-category-options"> <a {{action removePermission this}}><i class="icon icon-remove-sign"></i></a>
<label>{{i18n category.allowed_groups}}</label> </li>
<ul class="badge-list"> {{/each}}
{{#each groups}} </ul>
<li class="badge-group"> {{view Ember.Select contentBinding="availableGroups" valueBinding="selectedGroup"}}
{{this}} {{view Ember.Select class="permission-selector" optionValuePath="content.id" optionLabelPath="content.description" contentBinding="availablePermissions" valueBinding="selectedPermission"}}
<a {{action removeGroup this}}><i class="icon icon-remove-sign"></i></a> <button {{action addPermission selectedGroup selectedPermission}} class="btn btn-small">{{i18n category.add_group}}</button>
</li>
{{/each}}
</ul>
{{view Ember.Select contentBinding="availableGroups" valueBinding="selectedGroup"}}
<button {{action addGroup}} class="btn btn-small">{{i18n category.add_group}}</button>
</div>
{{/if}}
</section> </section>
</div> </div>
<div {{bindAttr class=":modal-tab :options-tab settingsSelected::invisible"}}> <div {{bindAttr class=":modal-tab :options-tab settingsSelected::invisible"}}>

View File

@ -3,7 +3,6 @@
<div class='quote-controls'></div> <div class='quote-controls'></div>
{{{avatarImg}}} {{{avatarImg}}}
{{username}} {{username}}
said:
</div> </div>
<blockquote>{{{quote}}}</blockquote> <blockquote>{{{quote}}}</blockquote>
</aside> </aside>

View File

@ -35,7 +35,7 @@
<dt>{{i18n user.last_seen}}:</dt><dd>{{date last_seen_at}}</dd> <dt>{{i18n user.last_seen}}:</dt><dd>{{date last_seen_at}}</dd>
{{/if}} {{/if}}
{{#if invited_by}} {{#if invited_by}}
<dt>{{i18n user.invited_by}}:</dt><dd>{{#linkTo user.activity invited_by}}{{invited_by.username}}{{/linkTo}}</dd> <dt>{{i18n user.invited_by}}:</dt><dd>{{#linkTo 'user.activity' invited_by}}{{invited_by.username}}{{/linkTo}}</dd>
{{/if}} {{/if}}
{{#if email}} {{#if email}}
<dt>{{i18n user.email.title}}:</dt><dd {{bindAttr title="email"}}>{{email}}</dd> <dt>{{i18n user.email.title}}:</dt><dd {{bindAttr title="email"}}>{{email}}</dd>

View File

@ -14,7 +14,10 @@ Discourse.CategoryChooserView = Discourse.ComboboxView.extend({
init: function() { init: function() {
this._super(); this._super();
this.set('content', Discourse.Category.list()); // TODO perhaps allow passing a param in to select if we need full or not
this.set('content', _.filter(Discourse.Category.list(), function(c){
return c.permission === Discourse.PermissionType.FULL;
}));
}, },
none: function() { none: function() {

View File

@ -37,11 +37,13 @@ Discourse.ActivityFilterView = Discourse.View.extend({
} }
var icon = this.get('icon'); var icon = this.get('icon');
buffer.push("<a href='#'>");
if(icon) { if(icon) {
buffer.push("<i class='glyph icon icon-" + icon + "'></i>"); buffer.push("<i class='glyph icon icon-" + icon + "'></i> ");
} }
buffer.push("<a href='#'>" + description + buffer.push(description +
" <span class='count'>(" + count + ")</span>"); " <span class='count'>(" + count + ")</span>");

View File

@ -29,13 +29,14 @@ var Handlebars = {};
; ;
// lib/handlebars/base.js // lib/handlebars/base.js
Handlebars.VERSION = "1.0.0-rc.4"; Handlebars.VERSION = "1.0.0";
Handlebars.COMPILER_REVISION = 3; Handlebars.COMPILER_REVISION = 4;
Handlebars.REVISION_CHANGES = { Handlebars.REVISION_CHANGES = {
1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
2: '== 1.0.0-rc.3', 2: '== 1.0.0-rc.3',
3: '>= 1.0.0-rc.4' 3: '== 1.0.0-rc.4',
4: '>= 1.0.0'
}; };
Handlebars.helpers = {}; Handlebars.helpers = {};
@ -67,7 +68,7 @@ Handlebars.registerHelper('helperMissing', function(arg) {
if(arguments.length === 2) { if(arguments.length === 2) {
return undefined; return undefined;
} else { } else {
throw new Error("Could not find property '" + arg + "'"); throw new Error("Missing helper: '" + arg + "'");
} }
}); });
@ -124,6 +125,9 @@ Handlebars.registerHelper('each', function(context, options) {
var fn = options.fn, inverse = options.inverse; var fn = options.fn, inverse = options.inverse;
var i = 0, ret = "", data; var i = 0, ret = "", data;
var type = toString.call(context);
if(type === functionType) { context = context.call(this); }
if (options.data) { if (options.data) {
data = Handlebars.createFrame(options.data); data = Handlebars.createFrame(options.data);
} }
@ -152,22 +156,25 @@ Handlebars.registerHelper('each', function(context, options) {
return ret; return ret;
}); });
Handlebars.registerHelper('if', function(context, options) { Handlebars.registerHelper('if', function(conditional, options) {
var type = toString.call(context); var type = toString.call(conditional);
if(type === functionType) { context = context.call(this); } if(type === functionType) { conditional = conditional.call(this); }
if(!context || Handlebars.Utils.isEmpty(context)) { if(!conditional || Handlebars.Utils.isEmpty(conditional)) {
return options.inverse(this); return options.inverse(this);
} else { } else {
return options.fn(this); return options.fn(this);
} }
}); });
Handlebars.registerHelper('unless', function(context, options) { Handlebars.registerHelper('unless', function(conditional, options) {
return Handlebars.helpers['if'].call(this, context, {fn: options.inverse, inverse: options.fn}); return Handlebars.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn});
}); });
Handlebars.registerHelper('with', function(context, options) { Handlebars.registerHelper('with', function(context, options) {
var type = toString.call(context);
if(type === functionType) { context = context.call(this); }
if (!Handlebars.Utils.isEmpty(context)) return options.fn(context); if (!Handlebars.Utils.isEmpty(context)) return options.fn(context);
}); });
@ -181,9 +188,9 @@ Handlebars.registerHelper('log', function(context, options) {
var handlebars = (function(){ var handlebars = (function(){
var parser = {trace: function trace() { }, var parser = {trace: function trace() { },
yy: {}, yy: {},
symbols_: {"error":2,"root":3,"program":4,"EOF":5,"simpleInverse":6,"statements":7,"statement":8,"openInverse":9,"closeBlock":10,"openBlock":11,"mustache":12,"partial":13,"CONTENT":14,"COMMENT":15,"OPEN_BLOCK":16,"inMustache":17,"CLOSE":18,"OPEN_INVERSE":19,"OPEN_ENDBLOCK":20,"path":21,"OPEN":22,"OPEN_UNESCAPED":23,"OPEN_PARTIAL":24,"partialName":25,"params":26,"hash":27,"DATA":28,"param":29,"STRING":30,"INTEGER":31,"BOOLEAN":32,"hashSegments":33,"hashSegment":34,"ID":35,"EQUALS":36,"PARTIAL_NAME":37,"pathSegments":38,"SEP":39,"$accept":0,"$end":1}, symbols_: {"error":2,"root":3,"program":4,"EOF":5,"simpleInverse":6,"statements":7,"statement":8,"openInverse":9,"closeBlock":10,"openBlock":11,"mustache":12,"partial":13,"CONTENT":14,"COMMENT":15,"OPEN_BLOCK":16,"inMustache":17,"CLOSE":18,"OPEN_INVERSE":19,"OPEN_ENDBLOCK":20,"path":21,"OPEN":22,"OPEN_UNESCAPED":23,"CLOSE_UNESCAPED":24,"OPEN_PARTIAL":25,"partialName":26,"params":27,"hash":28,"dataName":29,"param":30,"STRING":31,"INTEGER":32,"BOOLEAN":33,"hashSegments":34,"hashSegment":35,"ID":36,"EQUALS":37,"DATA":38,"pathSegments":39,"SEP":40,"$accept":0,"$end":1},
terminals_: {2:"error",5:"EOF",14:"CONTENT",15:"COMMENT",16:"OPEN_BLOCK",18:"CLOSE",19:"OPEN_INVERSE",20:"OPEN_ENDBLOCK",22:"OPEN",23:"OPEN_UNESCAPED",24:"OPEN_PARTIAL",28:"DATA",30:"STRING",31:"INTEGER",32:"BOOLEAN",35:"ID",36:"EQUALS",37:"PARTIAL_NAME",39:"SEP"}, terminals_: {2:"error",5:"EOF",14:"CONTENT",15:"COMMENT",16:"OPEN_BLOCK",18:"CLOSE",19:"OPEN_INVERSE",20:"OPEN_ENDBLOCK",22:"OPEN",23:"OPEN_UNESCAPED",24:"CLOSE_UNESCAPED",25:"OPEN_PARTIAL",31:"STRING",32:"INTEGER",33:"BOOLEAN",36:"ID",37:"EQUALS",38:"DATA",40:"SEP"},
productions_: [0,[3,2],[4,2],[4,3],[4,2],[4,1],[4,1],[4,0],[7,1],[7,2],[8,3],[8,3],[8,1],[8,1],[8,1],[8,1],[11,3],[9,3],[10,3],[12,3],[12,3],[13,3],[13,4],[6,2],[17,3],[17,2],[17,2],[17,1],[17,1],[26,2],[26,1],[29,1],[29,1],[29,1],[29,1],[29,1],[27,1],[33,2],[33,1],[34,3],[34,3],[34,3],[34,3],[34,3],[25,1],[21,1],[38,3],[38,1]], productions_: [0,[3,2],[4,2],[4,3],[4,2],[4,1],[4,1],[4,0],[7,1],[7,2],[8,3],[8,3],[8,1],[8,1],[8,1],[8,1],[11,3],[9,3],[10,3],[12,3],[12,3],[13,3],[13,4],[6,2],[17,3],[17,2],[17,2],[17,1],[17,1],[27,2],[27,1],[30,1],[30,1],[30,1],[30,1],[30,1],[28,1],[34,2],[34,1],[35,3],[35,3],[35,3],[35,3],[35,3],[26,1],[26,1],[26,1],[29,2],[21,1],[39,3],[39,1]],
performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) { performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) {
var $0 = $$.length - 1; var $0 = $$.length - 1;
@ -224,7 +231,10 @@ case 17: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]);
break; break;
case 18: this.$ = $$[$0-1]; case 18: this.$ = $$[$0-1];
break; break;
case 19: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]); case 19:
// Parsing out the '&' escape token at this level saves ~500 bytes after min due to the removal of one parser node.
this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], $$[$0-2][2] === '&');
break; break;
case 20: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], true); case 20: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], true);
break; break;
@ -242,7 +252,7 @@ case 26: this.$ = [[$$[$0-1]], $$[$0]];
break; break;
case 27: this.$ = [[$$[$0]], null]; case 27: this.$ = [[$$[$0]], null];
break; break;
case 28: this.$ = [[new yy.DataNode($$[$0])], null]; case 28: this.$ = [[$$[$0]], null];
break; break;
case 29: $$[$0-1].push($$[$0]); this.$ = $$[$0-1]; case 29: $$[$0-1].push($$[$0]); this.$ = $$[$0-1];
break; break;
@ -256,7 +266,7 @@ case 33: this.$ = new yy.IntegerNode($$[$0]);
break; break;
case 34: this.$ = new yy.BooleanNode($$[$0]); case 34: this.$ = new yy.BooleanNode($$[$0]);
break; break;
case 35: this.$ = new yy.DataNode($$[$0]); case 35: this.$ = $$[$0];
break; break;
case 36: this.$ = new yy.HashNode($$[$0]); case 36: this.$ = new yy.HashNode($$[$0]);
break; break;
@ -272,20 +282,26 @@ case 41: this.$ = [$$[$0-2], new yy.IntegerNode($$[$0])];
break; break;
case 42: this.$ = [$$[$0-2], new yy.BooleanNode($$[$0])]; case 42: this.$ = [$$[$0-2], new yy.BooleanNode($$[$0])];
break; break;
case 43: this.$ = [$$[$0-2], new yy.DataNode($$[$0])]; case 43: this.$ = [$$[$0-2], $$[$0]];
break; break;
case 44: this.$ = new yy.PartialNameNode($$[$0]); case 44: this.$ = new yy.PartialNameNode($$[$0]);
break; break;
case 45: this.$ = new yy.IdNode($$[$0]); case 45: this.$ = new yy.PartialNameNode(new yy.StringNode($$[$0]));
break; break;
case 46: $$[$0-2].push($$[$0]); this.$ = $$[$0-2]; case 46: this.$ = new yy.PartialNameNode(new yy.IntegerNode($$[$0]));
break; break;
case 47: this.$ = [$$[$0]]; case 47: this.$ = new yy.DataNode($$[$0]);
break;
case 48: this.$ = new yy.IdNode($$[$0]);
break;
case 49: $$[$0-2].push({part: $$[$0], separator: $$[$0-1]}); this.$ = $$[$0-2];
break;
case 50: this.$ = [{part: $$[$0]}];
break; break;
} }
}, },
table: [{3:1,4:2,5:[2,7],6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],22:[1,14],23:[1,15],24:[1,16]},{1:[3]},{5:[1,17]},{5:[2,6],7:18,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,6],22:[1,14],23:[1,15],24:[1,16]},{5:[2,5],6:20,8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,5],22:[1,14],23:[1,15],24:[1,16]},{17:23,18:[1,22],21:24,28:[1,25],35:[1,27],38:26},{5:[2,8],14:[2,8],15:[2,8],16:[2,8],19:[2,8],20:[2,8],22:[2,8],23:[2,8],24:[2,8]},{4:28,6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,7],22:[1,14],23:[1,15],24:[1,16]},{4:29,6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,7],22:[1,14],23:[1,15],24:[1,16]},{5:[2,12],14:[2,12],15:[2,12],16:[2,12],19:[2,12],20:[2,12],22:[2,12],23:[2,12],24:[2,12]},{5:[2,13],14:[2,13],15:[2,13],16:[2,13],19:[2,13],20:[2,13],22:[2,13],23:[2,13],24:[2,13]},{5:[2,14],14:[2,14],15:[2,14],16:[2,14],19:[2,14],20:[2,14],22:[2,14],23:[2,14],24:[2,14]},{5:[2,15],14:[2,15],15:[2,15],16:[2,15],19:[2,15],20:[2,15],22:[2,15],23:[2,15],24:[2,15]},{17:30,21:24,28:[1,25],35:[1,27],38:26},{17:31,21:24,28:[1,25],35:[1,27],38:26},{17:32,21:24,28:[1,25],35:[1,27],38:26},{25:33,37:[1,34]},{1:[2,1]},{5:[2,2],8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,2],22:[1,14],23:[1,15],24:[1,16]},{17:23,21:24,28:[1,25],35:[1,27],38:26},{5:[2,4],7:35,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,4],22:[1,14],23:[1,15],24:[1,16]},{5:[2,9],14:[2,9],15:[2,9],16:[2,9],19:[2,9],20:[2,9],22:[2,9],23:[2,9],24:[2,9]},{5:[2,23],14:[2,23],15:[2,23],16:[2,23],19:[2,23],20:[2,23],22:[2,23],23:[2,23],24:[2,23]},{18:[1,36]},{18:[2,27],21:41,26:37,27:38,28:[1,45],29:39,30:[1,42],31:[1,43],32:[1,44],33:40,34:46,35:[1,47],38:26},{18:[2,28]},{18:[2,45],28:[2,45],30:[2,45],31:[2,45],32:[2,45],35:[2,45],39:[1,48]},{18:[2,47],28:[2,47],30:[2,47],31:[2,47],32:[2,47],35:[2,47],39:[2,47]},{10:49,20:[1,50]},{10:51,20:[1,50]},{18:[1,52]},{18:[1,53]},{18:[1,54]},{18:[1,55],21:56,35:[1,27],38:26},{18:[2,44],35:[2,44]},{5:[2,3],8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,3],22:[1,14],23:[1,15],24:[1,16]},{14:[2,17],15:[2,17],16:[2,17],19:[2,17],20:[2,17],22:[2,17],23:[2,17],24:[2,17]},{18:[2,25],21:41,27:57,28:[1,45],29:58,30:[1,42],31:[1,43],32:[1,44],33:40,34:46,35:[1,47],38:26},{18:[2,26]},{18:[2,30],28:[2,30],30:[2,30],31:[2,30],32:[2,30],35:[2,30]},{18:[2,36],34:59,35:[1,60]},{18:[2,31],28:[2,31],30:[2,31],31:[2,31],32:[2,31],35:[2,31]},{18:[2,32],28:[2,32],30:[2,32],31:[2,32],32:[2,32],35:[2,32]},{18:[2,33],28:[2,33],30:[2,33],31:[2,33],32:[2,33],35:[2,33]},{18:[2,34],28:[2,34],30:[2,34],31:[2,34],32:[2,34],35:[2,34]},{18:[2,35],28:[2,35],30:[2,35],31:[2,35],32:[2,35],35:[2,35]},{18:[2,38],35:[2,38]},{18:[2,47],28:[2,47],30:[2,47],31:[2,47],32:[2,47],35:[2,47],36:[1,61],39:[2,47]},{35:[1,62]},{5:[2,10],14:[2,10],15:[2,10],16:[2,10],19:[2,10],20:[2,10],22:[2,10],23:[2,10],24:[2,10]},{21:63,35:[1,27],38:26},{5:[2,11],14:[2,11],15:[2,11],16:[2,11],19:[2,11],20:[2,11],22:[2,11],23:[2,11],24:[2,11]},{14:[2,16],15:[2,16],16:[2,16],19:[2,16],20:[2,16],22:[2,16],23:[2,16],24:[2,16]},{5:[2,19],14:[2,19],15:[2,19],16:[2,19],19:[2,19],20:[2,19],22:[2,19],23:[2,19],24:[2,19]},{5:[2,20],14:[2,20],15:[2,20],16:[2,20],19:[2,20],20:[2,20],22:[2,20],23:[2,20],24:[2,20]},{5:[2,21],14:[2,21],15:[2,21],16:[2,21],19:[2,21],20:[2,21],22:[2,21],23:[2,21],24:[2,21]},{18:[1,64]},{18:[2,24]},{18:[2,29],28:[2,29],30:[2,29],31:[2,29],32:[2,29],35:[2,29]},{18:[2,37],35:[2,37]},{36:[1,61]},{21:65,28:[1,69],30:[1,66],31:[1,67],32:[1,68],35:[1,27],38:26},{18:[2,46],28:[2,46],30:[2,46],31:[2,46],32:[2,46],35:[2,46],39:[2,46]},{18:[1,70]},{5:[2,22],14:[2,22],15:[2,22],16:[2,22],19:[2,22],20:[2,22],22:[2,22],23:[2,22],24:[2,22]},{18:[2,39],35:[2,39]},{18:[2,40],35:[2,40]},{18:[2,41],35:[2,41]},{18:[2,42],35:[2,42]},{18:[2,43],35:[2,43]},{5:[2,18],14:[2,18],15:[2,18],16:[2,18],19:[2,18],20:[2,18],22:[2,18],23:[2,18],24:[2,18]}], table: [{3:1,4:2,5:[2,7],6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],22:[1,14],23:[1,15],25:[1,16]},{1:[3]},{5:[1,17]},{5:[2,6],7:18,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,6],22:[1,14],23:[1,15],25:[1,16]},{5:[2,5],6:20,8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,5],22:[1,14],23:[1,15],25:[1,16]},{17:23,18:[1,22],21:24,29:25,36:[1,28],38:[1,27],39:26},{5:[2,8],14:[2,8],15:[2,8],16:[2,8],19:[2,8],20:[2,8],22:[2,8],23:[2,8],25:[2,8]},{4:29,6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,7],22:[1,14],23:[1,15],25:[1,16]},{4:30,6:3,7:4,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,5],20:[2,7],22:[1,14],23:[1,15],25:[1,16]},{5:[2,12],14:[2,12],15:[2,12],16:[2,12],19:[2,12],20:[2,12],22:[2,12],23:[2,12],25:[2,12]},{5:[2,13],14:[2,13],15:[2,13],16:[2,13],19:[2,13],20:[2,13],22:[2,13],23:[2,13],25:[2,13]},{5:[2,14],14:[2,14],15:[2,14],16:[2,14],19:[2,14],20:[2,14],22:[2,14],23:[2,14],25:[2,14]},{5:[2,15],14:[2,15],15:[2,15],16:[2,15],19:[2,15],20:[2,15],22:[2,15],23:[2,15],25:[2,15]},{17:31,21:24,29:25,36:[1,28],38:[1,27],39:26},{17:32,21:24,29:25,36:[1,28],38:[1,27],39:26},{17:33,21:24,29:25,36:[1,28],38:[1,27],39:26},{21:35,26:34,31:[1,36],32:[1,37],36:[1,28],39:26},{1:[2,1]},{5:[2,2],8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,2],22:[1,14],23:[1,15],25:[1,16]},{17:23,21:24,29:25,36:[1,28],38:[1,27],39:26},{5:[2,4],7:38,8:6,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,4],22:[1,14],23:[1,15],25:[1,16]},{5:[2,9],14:[2,9],15:[2,9],16:[2,9],19:[2,9],20:[2,9],22:[2,9],23:[2,9],25:[2,9]},{5:[2,23],14:[2,23],15:[2,23],16:[2,23],19:[2,23],20:[2,23],22:[2,23],23:[2,23],25:[2,23]},{18:[1,39]},{18:[2,27],21:44,24:[2,27],27:40,28:41,29:48,30:42,31:[1,45],32:[1,46],33:[1,47],34:43,35:49,36:[1,50],38:[1,27],39:26},{18:[2,28],24:[2,28]},{18:[2,48],24:[2,48],31:[2,48],32:[2,48],33:[2,48],36:[2,48],38:[2,48],40:[1,51]},{21:52,36:[1,28],39:26},{18:[2,50],24:[2,50],31:[2,50],32:[2,50],33:[2,50],36:[2,50],38:[2,50],40:[2,50]},{10:53,20:[1,54]},{10:55,20:[1,54]},{18:[1,56]},{18:[1,57]},{24:[1,58]},{18:[1,59],21:60,36:[1,28],39:26},{18:[2,44],36:[2,44]},{18:[2,45],36:[2,45]},{18:[2,46],36:[2,46]},{5:[2,3],8:21,9:7,11:8,12:9,13:10,14:[1,11],15:[1,12],16:[1,13],19:[1,19],20:[2,3],22:[1,14],23:[1,15],25:[1,16]},{14:[2,17],15:[2,17],16:[2,17],19:[2,17],20:[2,17],22:[2,17],23:[2,17],25:[2,17]},{18:[2,25],21:44,24:[2,25],28:61,29:48,30:62,31:[1,45],32:[1,46],33:[1,47],34:43,35:49,36:[1,50],38:[1,27],39:26},{18:[2,26],24:[2,26]},{18:[2,30],24:[2,30],31:[2,30],32:[2,30],33:[2,30],36:[2,30],38:[2,30]},{18:[2,36],24:[2,36],35:63,36:[1,64]},{18:[2,31],24:[2,31],31:[2,31],32:[2,31],33:[2,31],36:[2,31],38:[2,31]},{18:[2,32],24:[2,32],31:[2,32],32:[2,32],33:[2,32],36:[2,32],38:[2,32]},{18:[2,33],24:[2,33],31:[2,33],32:[2,33],33:[2,33],36:[2,33],38:[2,33]},{18:[2,34],24:[2,34],31:[2,34],32:[2,34],33:[2,34],36:[2,34],38:[2,34]},{18:[2,35],24:[2,35],31:[2,35],32:[2,35],33:[2,35],36:[2,35],38:[2,35]},{18:[2,38],24:[2,38],36:[2,38]},{18:[2,50],24:[2,50],31:[2,50],32:[2,50],33:[2,50],36:[2,50],37:[1,65],38:[2,50],40:[2,50]},{36:[1,66]},{18:[2,47],24:[2,47],31:[2,47],32:[2,47],33:[2,47],36:[2,47],38:[2,47]},{5:[2,10],14:[2,10],15:[2,10],16:[2,10],19:[2,10],20:[2,10],22:[2,10],23:[2,10],25:[2,10]},{21:67,36:[1,28],39:26},{5:[2,11],14:[2,11],15:[2,11],16:[2,11],19:[2,11],20:[2,11],22:[2,11],23:[2,11],25:[2,11]},{14:[2,16],15:[2,16],16:[2,16],19:[2,16],20:[2,16],22:[2,16],23:[2,16],25:[2,16]},{5:[2,19],14:[2,19],15:[2,19],16:[2,19],19:[2,19],20:[2,19],22:[2,19],23:[2,19],25:[2,19]},{5:[2,20],14:[2,20],15:[2,20],16:[2,20],19:[2,20],20:[2,20],22:[2,20],23:[2,20],25:[2,20]},{5:[2,21],14:[2,21],15:[2,21],16:[2,21],19:[2,21],20:[2,21],22:[2,21],23:[2,21],25:[2,21]},{18:[1,68]},{18:[2,24],24:[2,24]},{18:[2,29],24:[2,29],31:[2,29],32:[2,29],33:[2,29],36:[2,29],38:[2,29]},{18:[2,37],24:[2,37],36:[2,37]},{37:[1,65]},{21:69,29:73,31:[1,70],32:[1,71],33:[1,72],36:[1,28],38:[1,27],39:26},{18:[2,49],24:[2,49],31:[2,49],32:[2,49],33:[2,49],36:[2,49],38:[2,49],40:[2,49]},{18:[1,74]},{5:[2,22],14:[2,22],15:[2,22],16:[2,22],19:[2,22],20:[2,22],22:[2,22],23:[2,22],25:[2,22]},{18:[2,39],24:[2,39],36:[2,39]},{18:[2,40],24:[2,40],36:[2,40]},{18:[2,41],24:[2,41],36:[2,41]},{18:[2,42],24:[2,42],36:[2,42]},{18:[2,43],24:[2,43],36:[2,43]},{5:[2,18],14:[2,18],15:[2,18],16:[2,18],19:[2,18],20:[2,18],22:[2,18],23:[2,18],25:[2,18]}],
defaultActions: {17:[2,1],25:[2,28],38:[2,26],57:[2,24]}, defaultActions: {17:[2,1]},
parseError: function parseError(str, hash) { parseError: function parseError(str, hash) {
throw new Error(str); throw new Error(str);
}, },
@ -584,7 +600,7 @@ case 3:
break; break;
case 4: yy_.yytext = yy_.yytext.substr(0, yy_.yyleng-4); this.popState(); return 15; case 4: yy_.yytext = yy_.yytext.substr(0, yy_.yyleng-4); this.popState(); return 15;
break; break;
case 5: this.begin("par"); return 24; case 5: return 25;
break; break;
case 6: return 16; case 6: return 16;
break; break;
@ -596,7 +612,7 @@ case 9: return 19;
break; break;
case 10: return 23; case 10: return 23;
break; break;
case 11: return 23; case 11: return 22;
break; break;
case 12: this.popState(); this.begin('com'); case 12: this.popState(); this.begin('com');
break; break;
@ -604,48 +620,44 @@ case 13: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return
break; break;
case 14: return 22; case 14: return 22;
break; break;
case 15: return 36; case 15: return 37;
break; break;
case 16: return 35; case 16: return 36;
break; break;
case 17: return 35; case 17: return 36;
break; break;
case 18: return 39; case 18: return 40;
break; break;
case 19: /*ignore whitespace*/ case 19: /*ignore whitespace*/
break; break;
case 20: this.popState(); return 18; case 20: this.popState(); return 24;
break; break;
case 21: this.popState(); return 18; case 21: this.popState(); return 18;
break; break;
case 22: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 30; case 22: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 31;
break; break;
case 23: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\'/g,"'"); return 30; case 23: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\'/g,"'"); return 31;
break; break;
case 24: yy_.yytext = yy_.yytext.substr(1); return 28; case 24: return 38;
break; break;
case 25: return 32; case 25: return 33;
break; break;
case 26: return 32; case 26: return 33;
break; break;
case 27: return 31; case 27: return 32;
break; break;
case 28: return 35; case 28: return 36;
break; break;
case 29: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 35; case 29: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 36;
break; break;
case 30: return 'INVALID'; case 30: return 'INVALID';
break; break;
case 31: /*ignore whitespace*/ case 31: return 5;
break;
case 32: this.popState(); return 37;
break;
case 33: return 5;
break; break;
} }
}; };
lexer.rules = [/^(?:\\\\(?=(\{\{)))/,/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[}/ ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@[a-zA-Z]+)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:-?[0-9]+(?=[}\s]))/,/^(?:[a-zA-Z0-9_$:\-]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:\s+)/,/^(?:[a-zA-Z0-9_$\-\/]+)/,/^(?:$)/]; lexer.rules = [/^(?:\\\\(?=(\{\{)))/,/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[}\/ ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:-?[0-9]+(?=[}\s]))/,/^(?:[^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/];
lexer.conditions = {"mu":{"rules":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,33],"inclusive":false},"emu":{"rules":[3],"inclusive":false},"com":{"rules":[4],"inclusive":false},"par":{"rules":[31,32],"inclusive":false},"INITIAL":{"rules":[0,1,2,33],"inclusive":true}}; lexer.conditions = {"mu":{"rules":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31],"inclusive":false},"emu":{"rules":[3],"inclusive":false},"com":{"rules":[4],"inclusive":false},"INITIAL":{"rules":[0,1,2,31],"inclusive":true}};
return lexer;})() return lexer;})()
parser.lexer = lexer; parser.lexer = lexer;
function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser; function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser;
@ -731,21 +743,24 @@ Handlebars.AST.HashNode = function(pairs) {
Handlebars.AST.IdNode = function(parts) { Handlebars.AST.IdNode = function(parts) {
this.type = "ID"; this.type = "ID";
this.original = parts.join(".");
var dig = [], depth = 0; var original = "",
dig = [],
depth = 0;
for(var i=0,l=parts.length; i<l; i++) { for(var i=0,l=parts.length; i<l; i++) {
var part = parts[i]; var part = parts[i].part;
original += (parts[i].separator || '') + part;
if (part === ".." || part === "." || part === "this") { if (part === ".." || part === "." || part === "this") {
if (dig.length > 0) { throw new Handlebars.Exception("Invalid path: " + this.original); } if (dig.length > 0) { throw new Handlebars.Exception("Invalid path: " + original); }
else if (part === "..") { depth++; } else if (part === "..") { depth++; }
else { this.isScoped = true; } else { this.isScoped = true; }
} }
else { dig.push(part); } else { dig.push(part); }
} }
this.original = original;
this.parts = dig; this.parts = dig;
this.string = dig.join('.'); this.string = dig.join('.');
this.depth = depth; this.depth = depth;
@ -759,7 +774,7 @@ Handlebars.AST.IdNode = function(parts) {
Handlebars.AST.PartialNameNode = function(name) { Handlebars.AST.PartialNameNode = function(name) {
this.type = "PARTIAL_NAME"; this.type = "PARTIAL_NAME";
this.name = name; this.name = name.original;
}; };
Handlebars.AST.DataNode = function(id) { Handlebars.AST.DataNode = function(id) {
@ -769,13 +784,15 @@ Handlebars.AST.DataNode = function(id) {
Handlebars.AST.StringNode = function(string) { Handlebars.AST.StringNode = function(string) {
this.type = "STRING"; this.type = "STRING";
this.string = string; this.original =
this.stringModeValue = string; this.string =
this.stringModeValue = string;
}; };
Handlebars.AST.IntegerNode = function(integer) { Handlebars.AST.IntegerNode = function(integer) {
this.type = "INTEGER"; this.type = "INTEGER";
this.integer = integer; this.original =
this.integer = integer;
this.stringModeValue = Number(integer); this.stringModeValue = Number(integer);
}; };
@ -1162,7 +1179,15 @@ Compiler.prototype = {
DATA: function(data) { DATA: function(data) {
this.options.data = true; this.options.data = true;
this.opcode('lookupData', data.id); if (data.id.isScoped || data.id.depth) {
throw new Handlebars.Exception('Scoped data references are not supported: ' + data.original);
}
this.opcode('lookupData');
var parts = data.id.parts;
for(var i=0, l=parts.length; i<l; i++) {
this.opcode('lookup', parts[i]);
}
}, },
STRING: function(string) { STRING: function(string) {
@ -1361,8 +1386,9 @@ JavaScriptCompiler.prototype = {
if (!this.isChild) { if (!this.isChild) {
var namespace = this.namespace; var namespace = this.namespace;
var copies = "helpers = helpers || " + namespace + ".helpers;";
if (this.environment.usePartial) { copies = copies + " partials = partials || " + namespace + ".partials;"; } var copies = "helpers = this.merge(helpers, " + namespace + ".helpers);";
if (this.environment.usePartial) { copies = copies + " partials = this.merge(partials, " + namespace + ".partials);"; }
if (this.options.data) { copies = copies + " data = data || {};"; } if (this.options.data) { copies = copies + " data = data || {};"; }
out.push(copies); out.push(copies);
} else { } else {
@ -1391,7 +1417,9 @@ JavaScriptCompiler.prototype = {
// Generate minimizer alias mappings // Generate minimizer alias mappings
if (!this.isChild) { if (!this.isChild) {
for (var alias in this.context.aliases) { for (var alias in this.context.aliases) {
this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; if (this.context.aliases.hasOwnProperty(alias)) {
this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias];
}
} }
} }
@ -1610,7 +1638,7 @@ JavaScriptCompiler.prototype = {
// //
// Push the result of looking up `id` on the current data // Push the result of looking up `id` on the current data
lookupData: function(id) { lookupData: function(id) {
this.push(this.nameLookup('data', id, 'data')); this.push('data');
}, },
// [pushStringParam] // [pushStringParam]
@ -1717,8 +1745,9 @@ JavaScriptCompiler.prototype = {
this.context.aliases.helperMissing = 'helpers.helperMissing'; this.context.aliases.helperMissing = 'helpers.helperMissing';
var helper = this.lastHelper = this.setupHelper(paramSize, name, true); var helper = this.lastHelper = this.setupHelper(paramSize, name, true);
var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
this.push(helper.name); this.push(helper.name + ' || ' + nonHelper);
this.replaceStack(function(name) { this.replaceStack(function(name) {
return name + ' ? ' + name + '.call(' + return name + ' ? ' + name + '.call(' +
helper.callParams + ") " + ": helperMissing.call(" + helper.callParams + ") " + ": helperMissing.call(" +
@ -2163,6 +2192,16 @@ Handlebars.VM = {
} }
return programWrapper; return programWrapper;
}, },
merge: function(param, common) {
var ret = param || common;
if (param && common) {
ret = {};
Handlebars.Utils.extend(ret, common);
Handlebars.Utils.extend(ret, param);
}
return ret;
},
programWithDepth: Handlebars.VM.programWithDepth, programWithDepth: Handlebars.VM.programWithDepth,
noop: Handlebars.VM.noop, noop: Handlebars.VM.noop,
compilerInfo: null compilerInfo: null

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -32,30 +32,31 @@ PreloadStore = {
**/ **/
getAndRemove: function(key, finder) { getAndRemove: function(key, finder) {
var preloadStore = this; var preloadStore = this;
return Ember.Deferred.promise(function(promise) {
if (preloadStore.data[key]) {
promise.resolve(preloadStore.data[key]);
delete preloadStore.data[key];
} else {
if (finder) { if (preloadStore.data[key]) {
var result = finder(); var promise = Ember.RSVP.resolve(preloadStore.data[key]);
delete preloadStore.data[key];
return promise;
}
// If the finder returns a promise, we support that too if (finder) {
if (result.then) { return Ember.Deferred.promise(function(promise) {
result.then(function(result) { var result = finder();
return promise.resolve(result);
}, function(result) { // If the finder returns a promise, we support that too
return promise.reject(result); if (result.then) {
}); result.then(function(result) {
} else { return promise.resolve(result);
promise.resolve(result); }, function(result) {
} return promise.reject(result);
});
} else { } else {
promise.resolve(null); promise.resolve(result);
} }
} });
}); }
return Ember.RSVP.resolve(null);
}, },
/** /**

View File

@ -82,7 +82,7 @@
.badge-group { .badge-group {
@extend %badge; @extend %badge;
padding: 3px 2px 3px 8px; padding: 3px 5px;
color: $black; color: $black;
text-shadow: 0 1px 0 rgba($white, 0.2); text-shadow: 0 1px 0 rgba($white, 0.2);
background-color: #ddd; background-color: #ddd;

View File

@ -55,6 +55,7 @@
// -------------------------------------------------- // --------------------------------------------------
.nav-stacked { .nav-stacked {
position: relative;
@extend %nav; @extend %nav;
border: 1px solid $nav-stacked-border-color; border: 1px solid $nav-stacked-border-color;
padding: 0; padding: 0;
@ -69,7 +70,7 @@
} }
> a { > a {
margin: 0; margin: 0;
padding: 13px; padding: 13px 13px 13px 30px;
font-weight: bold; font-weight: bold;
font-size: 16px; font-size: 16px;
line-height: 20px; line-height: 20px;
@ -94,17 +95,11 @@
line-height: 20px; line-height: 20px;
} }
.no-glyph {
a {
padding-left: 30px;
}
}
.glyph { .glyph {
font-size: 12px; font-size: 12px;
margin: 15px 0 0 0;
width: 30px; width: 30px;
text-align: center; text-align: center;
float: left; position: absolute;
left: 2px;
} }
} }

View File

@ -102,7 +102,7 @@ class ApplicationController < ActionController::Base
guardian.current_user.sync_notification_channel_position guardian.current_user.sync_notification_channel_position
end end
store_preloaded("site", Site.cached_json(current_user)) store_preloaded("site", Site.cached_json(guardian))
if current_user.present? if current_user.present?
store_preloaded("currentUser", MultiJson.dump(CurrentUserSerializer.new(current_user, root: false))) store_preloaded("currentUser", MultiJson.dump(CurrentUserSerializer.new(current_user, root: false)))

View File

@ -52,16 +52,18 @@ class CategoriesController < ApplicationController
[:name, :color, :text_color] [:name, :color, :text_color]
end end
def category_param_keys
[required_param_keys, :hotness, :secure, :group_names, :auto_close_days].flatten!
end
def category_params def category_params
required_param_keys.each do |key| required_param_keys.each do |key|
params.require(key) params.require(key)
end end
params.permit(*category_param_keys) if p = params[:permissions]
p.each do |k,v|
p[k] = v.to_i
end
end
params.permit(*required_param_keys, :hotness, :auto_close_days, :permissions => [*p.try(:keys)])
end end
def fetch_category def fetch_category

View File

@ -27,6 +27,10 @@ class StaticController < ApplicationController
file = "static/#{page}.en" file = "static/#{page}.en"
end end
if not lookup_context.find_all("#{file}.html").any?
file = "static/#{page}"
end
if lookup_context.find_all("#{file}.html").any? if lookup_context.find_all("#{file}.html").any?
render file, layout: !request.xhr?, formats: [:html] render file, layout: !request.xhr?, formats: [:html]
return return

View File

@ -60,22 +60,7 @@ class Users::OmniauthCallbacksController < ApplicationController
auth_provider: "Twitter" auth_provider: "Twitter"
} }
if user_info process_user_info(user_info, screen_name)
if user_info.user.active?
if Guardian.new(user_info.user).can_access_forum?
log_on_user(user_info.user)
@data[:authenticated] = true
else
@data[:awaiting_approval] = true
end
else
@data[:awaiting_activation] = true
# send another email ?
end
else
@data[:name] = screen_name
end
end end
def create_or_sign_on_user_using_facebook(auth_token) def create_or_sign_on_user_using_facebook(auth_token)
@ -265,24 +250,7 @@ class Users::OmniauthCallbacksController < ApplicationController
auth_provider: "Github" auth_provider: "Github"
} }
if user_info process_user_info(user_info, screen_name)
if user_info.user.active?
if Guardian.new(user_info.user).can_access_forum?
log_on_user(user_info.user)
@data[:authenticated] = true
else
@data[:awaiting_approval] = true
end
else
@data[:awaiting_activation] = true
# send another email ?
end
else
@data[:name] = screen_name
end
end end
def create_or_sign_on_user_using_persona(auth_token) def create_or_sign_on_user_using_persona(auth_token)
@ -319,6 +287,26 @@ class Users::OmniauthCallbacksController < ApplicationController
private private
def process_user_info(user_info, screen_name)
if user_info
if user_info.user.active?
if Guardian.new(user_info.user).can_access_forum?
log_on_user(user_info.user)
@data[:authenticated] = true
else
@data[:awaiting_approval] = true
end
else
@data[:awaiting_activation] = true
# send another email ?
end
else
@data[:name] = screen_name
end
end
def invite_only? def invite_only?
SiteSetting.invite_only? && !@data[:authenticated] SiteSetting.invite_only? && !@data[:authenticated]
end end

View File

@ -22,6 +22,7 @@ class Category < ActiveRecord::Base
before_validation :ensure_slug before_validation :ensure_slug
after_save :invalidate_site_cache after_save :invalidate_site_cache
before_save :apply_permissions
after_create :create_category_definition after_create :create_category_definition
after_create :publish_categories_list after_create :publish_categories_list
after_destroy :invalidate_site_cache after_destroy :invalidate_site_cache
@ -34,15 +35,52 @@ class Category < ActiveRecord::Base
scope :secured, ->(guardian = nil) { scope :secured, ->(guardian = nil) {
ids = guardian.secure_category_ids if guardian ids = guardian.secure_category_ids if guardian
if ids.present? if ids.present?
where("NOT categories.secure or categories.id in (:cats)", cats: ids) where("NOT categories.read_restricted or categories.id in (:cats)", cats: ids)
else else
where("NOT categories.secure") where("NOT categories.read_restricted")
end end
} }
scope :topic_create_allowed, ->(guardian) {
scoped_to_permissions(guardian, [:full])
}
scope :post_create_allowed, ->(guardian) {
scoped_to_permissions(guardian, [:create_post, :full])
}
delegate :post_template, to: 'self.class' delegate :post_template, to: 'self.class'
attr_accessor :displayable_topics # permission is just used by serialization
# we may consider wrapping this in another spot
attr_accessor :displayable_topics, :permission
def self.scoped_to_permissions(guardian, permission_types)
if guardian && guardian.is_staff?
scoped
else
permission_types = permission_types.map{ |permission_type|
CategoryGroup.permission_types[permission_type]
}
where("categories.id in (
SELECT c.id FROM categories c
WHERE (
NOT c.read_restricted AND
(
NOT EXISTS(
SELECT 1 FROM category_groups cg WHERE cg.category_id = categories.id )
) OR EXISTS(
SELECT 1 FROM category_groups cg
WHERE permission_type in (?) AND
cg.category_id = categories.id AND
group_id IN (
SELECT g.group_id FROM group_users g where g.user_id = ? UNION SELECT ?
)
)
)
)", permission_types,(!guardian || guardian.user.blank?) ? -1 : guardian.user.id, Group[:everyone].id)
end
end
# Internal: Update category stats: # of topics in past year, month, week for # Internal: Update category stats: # of topics in past year, month, week for
# all categories. # all categories.
@ -119,28 +157,69 @@ class Category < ActiveRecord::Base
end end
end end
def deny(group) # will reset permission on a topic to a particular
if group == :all # set.
self.secure = true #
end # Available permissions are, :full, :create_post, :readonly
# hash can be:
#
# :everyone => :full - everyone has everything
# :everyone => :readonly, :staff => :full
# 7 => 1 # you can pass a group_id and permission id
def set_permissions(permissions)
self.read_restricted, @permissions = Category.resolve_permissions(permissions)
# Ideally we can just call .clear here, but it runs SQL, we only want to run it
# on save.
end end
def allow(group) def permissions=(permissions)
if group == :all set_permissions(permissions)
self.secure = false end
# this is kind of annoying, there should be a clean way of queuing this stuff
category_groups.destroy_all unless new_record? def apply_permissions
else if @permissions
groups.push(group) category_groups.destroy_all
@permissions.each do |group_id, permission_type|
category_groups.build(group_id: group_id, permission_type: permission_type)
end
@permissions = nil
end end
end end
def secure_group_ids def secure_group_ids
if self.secure if self.read_restricted?
groups.pluck("groups.id") groups.pluck("groups.id")
end end
end end
def self.resolve_permissions(permissions)
read_restricted = true
everyone = Group::AUTO_GROUPS[:everyone]
full = CategoryGroup.permission_types[:full]
mapped = permissions.map do |group,permission|
group = group.id if Group === group
# subtle, using Group[] ensures the group exists in the DB
group = Group[group.to_sym].id unless Fixnum === group
permission = CategoryGroup.permission_types[permission] unless Fixnum === permission
[group, permission]
end
mapped.each do |group, permission|
if group == everyone && permission == full
return [false, []]
end
read_restricted = false if group == everyone
end
[read_restricted, mapped]
end
end end
# == Schema Information # == Schema Information
@ -162,7 +241,7 @@ end
# description :text # description :text
# text_color :string(6) default("FFFFFF"), not null # text_color :string(6) default("FFFFFF"), not null
# hotness :float default(5.0), not null # hotness :float default(5.0), not null
# secure :boolean default(FALSE), not null # read_restricted :boolean default(FALSE), not null
# auto_close_days :float # auto_close_days :float
# #
# Indexes # Indexes

View File

@ -1,16 +1,22 @@
class CategoryGroup < ActiveRecord::Base class CategoryGroup < ActiveRecord::Base
belongs_to :category belongs_to :category
belongs_to :group belongs_to :group
def self.permission_types
@permission_types ||= Enum.new(:full, :create_post, :readonly)
end
end end
# == Schema Information # == Schema Information
# #
# Table name: category_groups # Table name: category_groups
# #
# id :integer not null, primary key # id :integer not null, primary key
# category_id :integer not null # category_id :integer not null
# group_id :integer not null # group_id :integer not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# permission_type :integer default(1)
# #

View File

@ -10,6 +10,7 @@ class Group < ActiveRecord::Base
validate :name_format_validator validate :name_format_validator
AUTO_GROUPS = { AUTO_GROUPS = {
:everyone => 0,
:admins => 1, :admins => 1,
:moderators => 2, :moderators => 2,
:staff => 3, :staff => 3,
@ -34,6 +35,10 @@ class Group < ActiveRecord::Base
group.save! group.save!
end end
# the everyone group is special, it can include non-users so there is no
# way to have the membership in a table
return group if name == :everyone
group.name = I18n.t("groups.default_names.#{name}") group.name = I18n.t("groups.default_names.#{name}")
# don't allow shoddy localization to break this # don't allow shoddy localization to break this

View File

@ -59,6 +59,7 @@ end
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# deleted_at :datetime # deleted_at :datetime
# deleted_by_id :integer
# #
# Indexes # Indexes
# #

View File

@ -412,6 +412,8 @@ end
# percent_rank :float default(1.0) # percent_rank :float default(1.0)
# notify_user_count :integer default(0), not null # notify_user_count :integer default(0), not null
# like_score :integer default(0), not null # like_score :integer default(0), not null
# deleted_by_id :integer
# nuked_user :boolean default(FALSE)
# #
# Indexes # Indexes
# #

View File

@ -63,9 +63,14 @@ class PostAnalyzer
@linked_hosts = {} @linked_hosts = {}
raw_links.each do |u| raw_links.each do |u|
uri = URI.parse(u) begin
host = uri.host uri = URI.parse(u)
@linked_hosts[host] ||= 1 host = uri.host
@linked_hosts[host] ||= 1
rescue URI::InvalidURIError
# An invalid URI does not count as a raw link.
next
end
end end
@linked_hosts @linked_hosts
end end

View File

@ -25,11 +25,24 @@ class Site
TrustLevel.all TrustLevel.all
end end
def group_names
@group_name ||= Group.pluck(:name)
end
def categories def categories
Category @categories ||= begin
.secured(@guardian) categories = Category
.latest .secured(@guardian)
.includes(:topic_only_relative_url) .latest
.includes(:topic_only_relative_url).to_a
allowed_topic_create = Set.new(Category.topic_create_allowed(@guardian).pluck(:id))
categories.each do |category|
category.permission = CategoryGroup.permission_types[:full] if allowed_topic_create.include?(category.id)
end
categories
end
end end
def archetypes def archetypes

View File

@ -103,9 +103,9 @@ class Topic < ActiveRecord::Base
# Query conditions # Query conditions
condition = condition =
if ids.present? if ids.present?
["NOT c.secure or c.id in (:cats)", cats: ids] ["NOT c.read_restricted or c.id in (:cats)", cats: ids]
else else
["NOT c.secure"] ["NOT c.read_restricted"]
end end
where("category_id IS NULL OR category_id IN ( where("category_id IS NULL OR category_id IN (
@ -629,8 +629,8 @@ class Topic < ActiveRecord::Base
self self
end end
def secure_category? def read_restricted_category?
category && category.secure category && category.read_restricted
end end
private private
@ -692,6 +692,7 @@ end
# auto_close_at :datetime # auto_close_at :datetime
# auto_close_user_id :integer # auto_close_user_id :integer
# auto_close_started_at :datetime # auto_close_started_at :datetime
# deleted_by_id :integer
# #
# Indexes # Indexes
# #

View File

@ -113,11 +113,11 @@ class TopicTrackingState
((#{unread}) OR (#{new})) AND ((#{unread}) OR (#{new})) AND
(topics.visible OR u.admin OR u.moderator) AND (topics.visible OR u.admin OR u.moderator) AND
topics.deleted_at IS NULL AND topics.deleted_at IS NULL AND
( category_id IS NULL OR NOT c.secure OR category_id IN ( ( category_id IS NULL OR NOT c.read_restricted OR category_id IN (
SELECT c2.id FROM categories c2 SELECT c2.id FROM categories c2
JOIN category_groups cg ON cg.category_id = c2.id JOIN category_groups cg ON cg.category_id = c2.id
JOIN group_users gu ON gu.user_id = u.id AND cg.group_id = gu.group_id JOIN group_users gu ON gu.user_id = u.id AND cg.group_id = gu.group_id
WHERE c2.secure ) WHERE c2.read_restricted )
) )
SQL SQL

View File

@ -486,10 +486,14 @@ class User < ActiveRecord::Base
end end
def secure_category_ids def secure_category_ids
cats = self.staff? ? Category.select(:id).where(secure: true) : secure_categories.select('categories.id') cats = self.staff? ? Category.select(:id).where(read_restricted: true) : secure_categories.select('categories.id')
cats.map { |c| c.id }.sort cats.map { |c| c.id }.sort
end end
def topic_create_allowed_category_ids
Category.topic_create_allowed(self.id).select(:id)
end
# Flag all posts from a user as spam # Flag all posts from a user as spam
def flag_linked_posts_as_spam def flag_linked_posts_as_spam
admin = Discourse.system_user admin = Discourse.system_user
@ -660,6 +664,7 @@ end
# topic_reply_count :integer default(0), not null # topic_reply_count :integer default(0), not null
# blocked :boolean default(FALSE) # blocked :boolean default(FALSE)
# dynamic_favicon :boolean default(FALSE), not null # dynamic_favicon :boolean default(FALSE), not null
# title :string(255)
# #
# Indexes # Indexes
# #

View File

@ -179,7 +179,7 @@ ORDER BY p.created_at desc
# move into Topic perhaps # move into Topic perhaps
group_ids = nil group_ids = nil
if topic && topic.category && topic.category.secure if topic && topic.category && topic.category.read_restricted
group_ids = topic.category.groups.pluck("groups.id") group_ids = topic.category.groups.pluck("groups.id")
end end
@ -232,11 +232,11 @@ ORDER BY p.created_at desc
unless guardian.is_staff? unless guardian.is_staff?
allowed = guardian.secure_category_ids allowed = guardian.secure_category_ids
if allowed.present? if allowed.present?
builder.where("( c.secure IS NULL OR builder.where("( c.read_restricted IS NULL OR
c.secure = 'f' OR NOT c.read_restricted OR
(c.secure = 't' and c.id in (:cats)) )", cats: guardian.secure_category_ids ) (c.read_restricted and c.id in (:cats)) )", cats: guardian.secure_category_ids )
else else
builder.where("(c.secure IS NULL OR c.secure = 'f')") builder.where("(c.read_restricted IS NULL OR NOT c.read_restricted)")
end end
end end
end end

View File

@ -9,6 +9,7 @@ class BasicCategorySerializer < ApplicationSerializer
:description, :description,
:topic_url, :topic_url,
:hotness, :hotness,
:secure :read_restricted,
:permission
end end

View File

@ -1,13 +1,24 @@
class CategorySerializer < BasicCategorySerializer class CategorySerializer < BasicCategorySerializer
attributes :secure, :groups, :available_groups, :auto_close_days attributes :read_restricted, :available_groups, :auto_close_days, :group_permissions
def groups def group_permissions
@groups ||= object.groups.order("name").all.map(&:name) @group_permissions ||= begin
perms = object.category_groups.joins(:group).includes(:group).order("groups.name").map do |cg|
{
permission_type: cg.permission_type,
group_name: cg.group.name
}
end
if perms.length == 0 && !object.read_restricted
perms << {permission_type: CategoryGroup.permission_types[:full], group_name: :everyone}
end
perms
end
end end
def available_groups def available_groups
Group.order("name").map(&:name) - groups Group.order(:name).pluck(:name) - group_permissions.map{|g| g[:group_name]}
end end
end end

View File

@ -3,7 +3,8 @@ class SiteSerializer < ApplicationSerializer
attributes :default_archetype, attributes :default_archetype,
:notification_types, :notification_types,
:post_types, :post_types,
:uncategorized_slug :uncategorized_slug,
:group_names
has_many :categories, serializer: BasicCategorySerializer, embed: :objects has_many :categories, serializer: BasicCategorySerializer, embed: :objects

View File

@ -112,7 +112,7 @@ module Discourse
# ember stuff only used for asset precompliation, production variant plays up # ember stuff only used for asset precompliation, production variant plays up
config.ember.variant = :development config.ember.variant = :development
config.ember.ember_location = "#{Rails.root}/app/assets/javascripts/external_production/ember.js" config.ember.ember_location = "#{Rails.root}/app/assets/javascripts/external_production/ember.js"
config.ember.handlebars_location = "#{Rails.root}/app/assets/javascripts/external/handlebars-1.0.rc.4.js" config.ember.handlebars_location = "#{Rails.root}/app/assets/javascripts/external/handlebars.js"
# Since we are using strong_parameters, we can disable and remove # Since we are using strong_parameters, we can disable and remove
# attr_accessible. # attr_accessible.

View File

@ -88,9 +88,8 @@ predef:
- visit - visit
- count - count
- exists - exists
- asyncTest - asyncTestDiscourse
- find - find
- resolvingPromise
- sinon - sinon
- controllerFor - controllerFor

View File

@ -12,7 +12,10 @@ cs:
storage_units: storage_units:
format: ! '%n %u' format: ! '%n %u'
units: units:
byte: B byte:
one: bajt
few: bajty
other: bajtů
gb: GB gb: GB
kb: KB kb: KB
mb: MB mb: MB
@ -666,6 +669,7 @@ cs:
description: "nebudete vůbec dostávat oznámení o tomto tématu a nebude se zobrazovat v seznamu nepřečtených témat." description: "nebudete vůbec dostávat oznámení o tomto tématu a nebude se zobrazovat v seznamu nepřečtených témat."
actions: actions:
recover: "Vrátit téma"
delete: "Odstranit téma" delete: "Odstranit téma"
open: "Otevřít téma" open: "Otevřít téma"
close: "Zavřít téma" close: "Zavřít téma"
@ -775,6 +779,7 @@ cs:
continue_discussion: "Pokračující diskuze z {{postLink}}:" continue_discussion: "Pokračující diskuze z {{postLink}}:"
follow_quote: "přejít na citovaný příspěvek" follow_quote: "přejít na citovaný příspěvek"
deleted_by_author: "(příspěvek odstraněn autorem)" deleted_by_author: "(příspěvek odstraněn autorem)"
deleted_by: "odstranil"
expand_collapse: "rozbalit/sbalit" expand_collapse: "rozbalit/sbalit"
has_replies: has_replies:
@ -789,6 +794,7 @@ cs:
upload_too_large: "Soubor, který se snažíte nahrát je bohužel příliš velký (maximální velikost je {{max_size_kb}}kb). Prosím zmenšete ho zkuste to znovu." upload_too_large: "Soubor, který se snažíte nahrát je bohužel příliš velký (maximální velikost je {{max_size_kb}}kb). Prosím zmenšete ho zkuste to znovu."
too_many_uploads: "Bohužel, najednou smíte nahrát jen jeden soubor." too_many_uploads: "Bohužel, najednou smíte nahrát jen jeden soubor."
upload_not_authorized: "Bohužel, soubor, který se snažíte nahrát, není povolený (povolené přípony: {{authorized_extensions}})." upload_not_authorized: "Bohužel, soubor, který se snažíte nahrát, není povolený (povolené přípony: {{authorized_extensions}})."
upload_not_allowed_for_new_user: "Bohužel, noví uživatelé nemohou nahrávat obrázky."
abandon: "Opravdu chcete opustit váš příspěvek?" abandon: "Opravdu chcete opustit váš příspěvek?"
@ -998,6 +1004,7 @@ cs:
views_long: "toto téma bylo zobrazeno {{number}}krát" views_long: "toto téma bylo zobrazeno {{number}}krát"
activity: "Aktivita" activity: "Aktivita"
likes: "Líbí se" likes: "Líbí se"
likes_long: "je zde {{number}} 'líbí se' v tomto tématu"
top_contributors: "Účastníci" top_contributors: "Účastníci"
category_title: "Kategorie" category_title: "Kategorie"
history: "Historie" history: "Historie"
@ -1064,6 +1071,8 @@ cs:
critical_available: "Je k dispozici důležitá aktualizace." critical_available: "Je k dispozici důležitá aktualizace."
updates_available: "Jsou k dispozici aktualizace." updates_available: "Jsou k dispozici aktualizace."
please_upgrade: "Prosím aktualizujte!" please_upgrade: "Prosím aktualizujte!"
no_check_performed: "Kontrola na aktualizace nebyla provedena. Ujistěte se, že běží služby clockword a sidekiq."
stale_data: "V poslední době neproběhal kontrola aktualizací. Ujistěte se, že běží služby clockword a sidekiq."
installed_version: "Nainstalováno" installed_version: "Nainstalováno"
latest_version: "Poslední verze" latest_version: "Poslední verze"
problems_found: "Byly nalezeny problémy s vaší instalací systému Discourse:" problems_found: "Byly nalezeny problémy s vaší instalací systému Discourse:"
@ -1073,6 +1082,7 @@ cs:
moderators: 'Moderátoři:' moderators: 'Moderátoři:'
admins: 'Administrátoři:' admins: 'Administrátoři:'
blocked: 'Blokováno:' blocked: 'Blokováno:'
banned: 'Zakázáno:'
private_messages_short: "SZ" private_messages_short: "SZ"
private_messages_title: "Soukromé zprávy" private_messages_title: "Soukromé zprávy"
@ -1176,6 +1186,7 @@ cs:
settings: "Nastavení" settings: "Nastavení"
logs: "Záznamy" logs: "Záznamy"
sent_at: "Odesláno" sent_at: "Odesláno"
user: "Uživatel"
email_type: "Typ emailu" email_type: "Typ emailu"
to_address: "Komu" to_address: "Komu"
test_email_address: "testovací emailová adresa" test_email_address: "testovací emailová adresa"
@ -1205,9 +1216,13 @@ cs:
not_found: "Bohužel uživatel s tímto jménem není v našem systému." not_found: "Bohužel uživatel s tímto jménem není v našem systému."
active: "Aktivní" active: "Aktivní"
nav: nav:
new: "Noví"
active: "Aktivní" active: "Aktivní"
new: "Nový"
pending: "Čeká na schválení" pending: "Čeká na schválení"
admins: "Administrátoři"
moderators: "Moderátoři"
banned: "Zakázaní"
blocked: "Blokovaní"
approved: "Schválen?" approved: "Schválen?"
approved_selected: approved_selected:
one: "schválit uživatele" one: "schválit uživatele"
@ -1225,6 +1240,7 @@ cs:
admins: 'Admininstrátoři' admins: 'Admininstrátoři'
moderators: 'Moderátoři' moderators: 'Moderátoři'
blocked: 'Blokovaní uživatelé' blocked: 'Blokovaní uživatelé'
banned: "Zakázaní uživatelé"
user: user:
ban_failed: "Nastala chyba při zakazování uživatele {{error}}" ban_failed: "Nastala chyba při zakazování uživatele {{error}}"
@ -1281,7 +1297,7 @@ cs:
deactivate_explanation: "Deaktivovaný uživatel musí znovu validovat svoji emailovou adresu než se bude moci znovu přihlásit." deactivate_explanation: "Deaktivovaný uživatel musí znovu validovat svoji emailovou adresu než se bude moci znovu přihlásit."
banned_explanation: "Zakázaný uživatel se nemůže přihlásit." banned_explanation: "Zakázaný uživatel se nemůže přihlásit."
block_explanation: "Zablokovaný uživatel nemůže přispívat nebo vytvářet nová témata." block_explanation: "Zablokovaný uživatel nemůže přispívat nebo vytvářet nová témata."
trust_level_change_failed: "Nastal problém při změně důveryhodnosti uživatele."
site_content: site_content:
none: "Zvolte typ obsahu a můžete začít editovat." none: "Zvolte typ obsahu a můžete začít editovat."

View File

@ -168,6 +168,7 @@ en:
"13": "Inbox" "13": "Inbox"
user: user:
said: "{{username}} said:"
profile: Profile profile: Profile
mute: Mute mute: Mute
edit: Edit Preferences edit: Edit Preferences
@ -999,6 +1000,11 @@ en:
browser_update: 'Unfortunately, <a href="http://www.discourse.org/faq/#browser">your browser is too old to work on this Discourse forum</a>. Please <a href="http://browsehappy.com">upgrade your browser</a>.' browser_update: 'Unfortunately, <a href="http://www.discourse.org/faq/#browser">your browser is too old to work on this Discourse forum</a>. Please <a href="http://browsehappy.com">upgrade your browser</a>.'
permission_types:
full: "Create Topics, Create Posts and Read"
create_post: "Create Posts and Read"
readonly: "Read Only"
# This section is exported to the javascript for i18n in the admin section # This section is exported to the javascript for i18n in the admin section
admin_js: admin_js:
type_to_filter: "type to filter..." type_to_filter: "type to filter..."
@ -1245,3 +1251,4 @@ en:
title: 'Settings' title: 'Settings'
reset: 'reset to default' reset: 'reset to default'
none: 'none' none: 'none'

View File

@ -60,6 +60,9 @@ cs:
rss_topics_in_category: "RSS feed témat z kategorie '%{category}'" rss_topics_in_category: "RSS feed témat z kategorie '%{category}'"
author_wrote: "%{author} napsal:" author_wrote: "%{author} napsal:"
private_message_abbrev: "SZ" private_message_abbrev: "SZ"
rss_description:
latest: "Poslední témata"
hot: "Populární témata"
groups: groups:
errors: errors:
@ -139,6 +142,8 @@ cs:
title: "vůdce" title: "vůdce"
elder: elder:
title: "starší" title: "starší"
change_failed_explanation: "Pokusili jste se změnil důvěryhodnost uživatele %{user_name} na '%{new_trust_level}'. Jejich důvěryhodnost je ale již '%{current_trust_level}'. %{user_name} zůstané na důvěryhodnosti '%{current_trust_level}'"
rate_limiter: rate_limiter:
too_many_requests: "Děláte tuto akci příliš často. Prosím počkejte %{time_left} a zkuste to znovu." too_many_requests: "Děláte tuto akci příliš často. Prosím počkejte %{time_left} a zkuste to znovu."
@ -501,6 +506,7 @@ cs:
apple_touch_icon_url: "Ikona používaná pro doteková zařízení od firmy Apple. Doporučené rozměry jsou 144px krát 144px." apple_touch_icon_url: "Ikona používaná pro doteková zařízení od firmy Apple. Doporučené rozměry jsou 144px krát 144px."
notification_email: "Návratová emailová adresa, která se použije u systémových emailů, jako jsou notifikace o zapomenutém heslu, nových účtech, atd." notification_email: "Návratová emailová adresa, která se použije u systémových emailů, jako jsou notifikace o zapomenutém heslu, nových účtech, atd."
email_custom_headers: "Seznam vlastních hlaviček emailů, oddělený svislítkem"
use_ssl: "Má být web přístupný přes SSL? (NEPODPOROVÁNO, EXPERIMENTÁLNÍ FUNKCE)" use_ssl: "Má být web přístupný přes SSL? (NEPODPOROVÁNO, EXPERIMENTÁLNÍ FUNKCE)"
best_of_score_threshold: "Minimální skóre příspěvku, aby byl zařazen mezi 'nejlepší'" best_of_score_threshold: "Minimální skóre příspěvku, aby byl zařazen mezi 'nejlepší'"
best_of_posts_required: "Minimální počet příspěvků v tématu, aby byl povolen mód 'nejlepší příspěvky'" best_of_posts_required: "Minimální počet příspěvků v tématu, aby byl povolen mód 'nejlepší příspěvky'"
@ -527,6 +533,8 @@ cs:
must_approve_users: "Administrátoři musí schválit všechny uživatele, než získají přístup" must_approve_users: "Administrátoři musí schválit všechny uživatele, než získají přístup"
ga_tracking_code: "Kód pro sledování přes 'Google analytics', např. UA-12345678-9; viz http://google.com/analytics" ga_tracking_code: "Kód pro sledování přes 'Google analytics', např. UA-12345678-9; viz http://google.com/analytics"
ga_domain_name: "Doménové jméno pro Google analytics, např. mysite.com; viz http://google.com/analytics" ga_domain_name: "Doménové jméno pro Google analytics, např. mysite.com; viz http://google.com/analytics"
enable_escaped_fragments: "Povolit alternativní řešení, které pomůže starým webovým robotům indexovat váš web. VAROVÁNÍ: povolte pouze v případě, že to opravdu potřebujete."
enable_noscript_support: "Povolit podporu &lt;noscipt&gt; tagu"
top_menu: "Určuje, které položky se zobrazí v navigaci na hlavní stránce a v jakém pořadí. Příklad: latest|hot|read|favorited|unread|new|posted|categories" top_menu: "Určuje, které položky se zobrazí v navigaci na hlavní stránce a v jakém pořadí. Příklad: latest|hot|read|favorited|unread|new|posted|categories"
post_menu: "Určuje, které položky se zobrazí v menu u příspěvku a v jakém pořadí. Příklad: like|edit|flag|delete|share|bookmark|reply" post_menu: "Určuje, které položky se zobrazí v menu u příspěvku a v jakém pořadí. Příklad: like|edit|flag|delete|share|bookmark|reply"
share_links: "Určuje, které položky se zobrazí ve sdílecím dialogu a v jakém pořadí. Příklad: twitter|facebook|google+" share_links: "Určuje, které položky se zobrazí ve sdílecím dialogu a v jakém pořadí. Příklad: twitter|facebook|google+"
@ -536,6 +544,8 @@ cs:
system_username: "Uživatelské jméno, za které se zasílají automatické soukromé zprávy" system_username: "Uživatelské jméno, za které se zasílají automatické soukromé zprávy"
send_welcome_message: "Mají noví uživatelé obdržet uvítací soukromou zprávu?" send_welcome_message: "Mají noví uživatelé obdržet uvítací soukromou zprávu?"
suppress_reply_directly_below: "Nezobrazovat počet odpovědí, pokud existuje jen jediná odpověď hned pod příspěvkem" suppress_reply_directly_below: "Nezobrazovat počet odpovědí, pokud existuje jen jediná odpověď hned pod příspěvkem"
suppress_reply_directly_above: "Nezobrazovat informaci 'je odpověď na' u příspěvku, jehož odpověď je přímo nad ním"
allow_index_in_robots_txt: "Povolit robotům indexaci tohoto webu (aktualizuje robots.txt)" allow_index_in_robots_txt: "Povolit robotům indexaci tohoto webu (aktualizuje robots.txt)"
email_domains_blacklist: "Seznam domén, jejichž emailové adresy nebudou přijímány, oddělený znakem '|'. Příklad: mailinator.com|trashmail.net" email_domains_blacklist: "Seznam domén, jejichž emailové adresy nebudou přijímány, oddělený znakem '|'. Příklad: mailinator.com|trashmail.net"
email_domains_whitelist: "Seznam domén, ze kterých bude povolena registrace, oddělený znamek '|'. POZOR: Emailové adresy z jiných než z těchto domén nebudou přijímány." email_domains_whitelist: "Seznam domén, ze kterých bude povolena registrace, oddělený znamek '|'. POZOR: Emailové adresy z jiných než z těchto domén nebudou přijímány."
@ -603,6 +613,8 @@ cs:
s3_secret_access_key: "Hodnota 'server access key' služby Amazon S3, která se použije pro nahrávání obrázku" s3_secret_access_key: "Hodnota 'server access key' služby Amazon S3, která se použije pro nahrávání obrázku"
s3_region: "Hodnota 'region' služby Amazon S3, která se použije pro nahrávání obrázku" s3_region: "Hodnota 'region' služby Amazon S3, která se použije pro nahrávání obrázku"
enable_flash_video_onebox: "Povolit embedování SWF a FLV odkazů do oneboxu (může znamenat bezpečnostní riziko, doporučujeme opatrnost)"
default_invitee_trust_level: "Výchozí věrohodnost pozvaných uživatelů (0-4)" default_invitee_trust_level: "Výchozí věrohodnost pozvaných uživatelů (0-4)"
default_trust_level: "Výchozí věrohodnost uživatelů (0-4)" default_trust_level: "Výchozí věrohodnost uživatelů (0-4)"
@ -663,7 +675,9 @@ cs:
pop3s_polling_username: "Uživatelské jméno pro dotazování přes POP3S" pop3s_polling_username: "Uživatelské jméno pro dotazování přes POP3S"
pop3s_polling_password: "Heslo pro dotazování přes POP3S" pop3s_polling_password: "Heslo pro dotazování přes POP3S"
minimum_topics_similar: "How many topics need to exist in the database before similar topics are presented." minimum_topics_similar: "Kolik témat musí v databázi existovat, než se začne nabízet menu s podobnými tématy."
relative_date_duration: "Počet dní od zaslání příspěvku, po které se budou datumy zobrazovat relativně namísto absolutně. Příklady: relativně: 7d, absolutně: 20. února"
notification_types: notification_types:

View File

@ -436,6 +436,9 @@ en:
tos_miscellaneous: tos_miscellaneous:
title: "Terms of Service: Miscellaneous" title: "Terms of Service: Miscellaneous"
description: "The text for the Miscellaneous section of the Terms of Service." description: "The text for the Miscellaneous section of the Terms of Service."
login_required:
title: "Login Required: Homepage"
description: "The text displayed for unauthorized users when login is required on the site."
site_settings: site_settings:
default_locale: "The default language of this Discourse instance (ISO 639-1 Code)" default_locale: "The default language of this Discourse instance (ISO 639-1 Code)"
@ -458,7 +461,7 @@ en:
company_short_name: "The short name of the company that runs this site, used in legal documents like the /tos" company_short_name: "The short name of the company that runs this site, used in legal documents like the /tos"
company_domain: "The domain name owned by the company that runs this site, used in legal documents like the /tos" company_domain: "The domain name owned by the company that runs this site, used in legal documents like the /tos"
api_key: "The secure API key used to create and update topics, use the /admin/api section to set it up" api_key: "The secure API key used to create and update topics, use the /admin/api section to set it up"
queue_jobs: "DEVELOPER ONLY! WARNING! Queue various jobs in sidekiq, if false queues are inline" queue_jobs: "DEVELOPER ONLY! WARNING! By default, queue jobs in sidekiq. If disabled, your site will be broken."
crawl_images: "Enable retrieving images from third party sources to insert width and height dimensions" crawl_images: "Enable retrieving images from third party sources to insert width and height dimensions"
ninja_edit_window: "Number of seconds after posting where edits do not create a new version" ninja_edit_window: "Number of seconds after posting where edits do not create a new version"
max_image_width: "Maximum allowed width of images in a post" max_image_width: "Maximum allowed width of images in a post"

View File

@ -0,0 +1,9 @@
class AddPermissionTypeToCategoryGroups < ActiveRecord::Migration
def change
# 1 is full permissions
add_column :category_groups, :permission_type, :integer, default: 1
# secure is ambiguous after this change, it should be read_restricted
rename_column :categories, :secure, :read_restricted
end
end

View File

@ -1,6 +1,6 @@
<table cellspacing="0" cellpadding="0" style="border: 1px solid #eee; -webkit-border-radius: 10px;"> <table cellspacing="0" cellpadding="0" style="border: 1px solid #eee; -webkit-border-radius: 10px;">
<tr> <tr>
<th style="text-align:left; background-color: #eee; padding: 5px">{{{avatarImg}}} {{username}} said:</th> <th style="text-align:left; background-color: #eee; padding: 5px">{{{avatarImg}}} {{username}}</th>
</tr> </tr>
<tr> <tr>
<td style="padding: 10px; background-color: #f9f9f9">{{{quote}}}</td> <td style="padding: 10px; background-color: #f9f9f9">{{{quote}}}</td>

View File

@ -7,6 +7,7 @@ class Guardian
def staff?; false; end def staff?; false; end
def approved?; false; end def approved?; false; end
def secure_category_ids; []; end def secure_category_ids; []; end
def topic_create_allowed_category_ids; []; end
def has_trust_level?(level); false; end def has_trust_level?(level); false; end
end end
@ -237,8 +238,19 @@ class Guardian
can_create_post?(parent) can_create_post?(parent)
end end
def can_create_topic_on_category?(category)
can_create_post?(nil) && (
!category ||
Category.topic_create_allowed(self).where(:id => category.id).count == 1
)
end
def can_create_post?(parent) def can_create_post?(parent)
!SpamRulesEnforcer.block?(@user) !SpamRulesEnforcer.block?(@user) && (
!parent ||
!parent.category ||
Category.post_create_allowed(self).where(:id => parent.category.id).count == 1
)
end end
def can_create_post_on_topic?(topic) def can_create_post_on_topic?(topic)
@ -328,7 +340,7 @@ class Guardian
topic.deleted_at.nil? && topic.deleted_at.nil? &&
# not secure, or I can see it # not secure, or I can see it
(not(topic.secure_category?) || can_see_category?(topic.category)) && (not(topic.read_restricted_category?) || can_see_category?(topic.category)) &&
# not private, or I am allowed (or an admin) # not private, or I am allowed (or an admin)
(not(topic.private_message?) || authenticated? && (topic.all_allowed_users.where(id: @user.id).exists? || is_admin?)) (not(topic.private_message?) || authenticated? && (topic.all_allowed_users.where(id: @user.id).exists? || is_admin?))
@ -340,7 +352,7 @@ class Guardian
end end
def can_see_category?(category) def can_see_category?(category)
not(category.secure) || secure_category_ids.include?(category.id) not(category.read_restricted) || secure_category_ids.include?(category.id)
end end
def can_vote?(post, opts={}) def can_vote?(post, opts={})
@ -378,6 +390,10 @@ class Guardian
@secure_category_ids ||= @user.secure_category_ids @secure_category_ids ||= @user.secure_category_ids
end end
def topic_create_allowed_category_ids
@topic_create_allowed_category_ids ||= @user.topic_create_allowed_category_ids
end
private private
def is_my_own?(obj) def is_my_own?(obj)

View File

@ -19,9 +19,9 @@ module Oneboxer
case route[:controller] case route[:controller]
when 'users' when 'users'
user = User.where(username_lower: route[:username].downcase).first user = User.where(username_lower: route[:username].downcase).first
return nil unless user return nil unless user
Guardian.new.ensure_can_see!(user) return @url unless Guardian.new.can_see?(user)
args.merge! avatar: PrettyText.avatar_img(user.username, 'tiny'), username: user.username args.merge! avatar: PrettyText.avatar_img(user.username, 'tiny'), username: user.username
args[:bio] = user.bio_cooked if user.bio_cooked.present? args[:bio] = user.bio_cooked if user.bio_cooked.present?
@ -33,7 +33,7 @@ module Oneboxer
post = Post.where(topic_id: route[:topic_id], post_number: route[:post_number].to_i).first post = Post.where(topic_id: route[:topic_id], post_number: route[:post_number].to_i).first
return nil unless post return nil unless post
Guardian.new.ensure_can_see!(post) return @url unless Guardian.new.can_see?(post)
topic = post.topic topic = post.topic
slug = Slug.for(topic.title) slug = Slug.for(topic.title)
@ -52,7 +52,8 @@ module Oneboxer
topic = Topic.where(id: route[:topic_id].to_i).includes(:user).first topic = Topic.where(id: route[:topic_id].to_i).includes(:user).first
return nil unless topic return nil unless topic
Guardian.new.ensure_can_see!(topic) return @url unless Guardian.new.can_see?(topic)
post = topic.posts.first post = topic.posts.first
posters = topic.posters_summary.map do |p| posters = topic.posters_summary.map do |p|

View File

@ -116,7 +116,7 @@ class PostCreator
protected protected
def secure_group_ids(topic) def secure_group_ids(topic)
@secure_group_ids ||= if topic.category && topic.category.secure? @secure_group_ids ||= if topic.category && topic.category.read_restricted?
topic.category.secure_group_ids topic.category.secure_group_ids
end end
end end

View File

@ -46,6 +46,19 @@ module PrettyText
class Helpers class Helpers
def t(key, opts)
str = I18n.t("js." + key)
if opts
# TODO: server localisation has no parity with client
# should be fixed
opts.each do |k,v|
str.gsub!("{{#{k}}}", v)
end
end
str
end
# function here are available to v8 # function here are available to v8
def avatar_template(username) def avatar_template(username)
return "" unless username return "" unless username
@ -90,6 +103,7 @@ module PrettyText
@ctx.eval("var Discourse = {}; Discourse.SiteSettings = #{SiteSetting.client_settings_json};") @ctx.eval("var Discourse = {}; Discourse.SiteSettings = #{SiteSetting.client_settings_json};")
@ctx.eval("var window = {}; window.devicePixelRatio = 2;") # hack to make code think stuff is retina @ctx.eval("var window = {}; window.devicePixelRatio = 2;") # hack to make code think stuff is retina
@ctx.eval("var I18n = {}; I18n.t = function(a,b){ return helpers.t(a,b); }");
ctx_load( "app/assets/javascripts/discourse/components/bbcode.js", ctx_load( "app/assets/javascripts/discourse/components/bbcode.js",
"app/assets/javascripts/discourse/components/utilities.js", "app/assets/javascripts/discourse/components/utilities.js",

View File

@ -160,9 +160,9 @@ class Search
.order("topics.bumped_at DESC") .order("topics.bumped_at DESC")
if secure_category_ids.present? if secure_category_ids.present?
posts = posts.where("(categories.id IS NULL) OR (NOT categories.secure) OR (categories.id IN (?))", secure_category_ids) posts = posts.where("(categories.id IS NULL) OR (NOT categories.read_restricted) OR (categories.id IN (?))", secure_category_ids)
else else
posts = posts.where("(categories.id IS NULL) OR (NOT categories.secure)") posts = posts.where("(categories.id IS NULL) OR (NOT categories.read_restricted)")
end end
posts.limit(limit) posts.limit(limit)
end end

View File

@ -18,9 +18,9 @@ class SqlBuilder
def secure_category(secure_category_ids, category_alias = 'c') def secure_category(secure_category_ids, category_alias = 'c')
if secure_category_ids.present? if secure_category_ids.present?
where("NOT COALESCE(" << category_alias << ".secure, false) OR " << category_alias << ".id IN (:secure_category_ids)", secure_category_ids: secure_category_ids) where("NOT COALESCE(" << category_alias << ".read_restricted, false) OR " << category_alias << ".id IN (:secure_category_ids)", secure_category_ids: secure_category_ids)
else else
where("NOT COALESCE(" << category_alias << ".secure, false)") where("NOT COALESCE(" << category_alias << ".read_restricted, false)")
end end
self self
end end

View File

@ -27,9 +27,9 @@ class TopicCreator
topic_params[:archetype] = @opts[:archetype] if @opts[:archetype].present? topic_params[:archetype] = @opts[:archetype] if @opts[:archetype].present?
topic_params[:subtype] = @opts[:subtype] if @opts[:subtype].present? topic_params[:subtype] = @opts[:subtype] if @opts[:subtype].present?
@guardian.ensure_can_create!(Topic)
category = Category.where(name: @opts[:category]).first category = Category.where(name: @opts[:category]).first
@guardian.ensure_can_create!(Topic,category)
topic_params[:category_id] = category.id if category.present? topic_params[:category_id] = category.id if category.present?
topic_params[:meta_data] = @opts[:meta_data] if @opts[:meta_data].present? topic_params[:meta_data] = @opts[:meta_data] if @opts[:meta_data].present?
topic_params[:created_at] = Time.zone.parse(@opts[:created_at].to_s) if @opts[:created_at].present? topic_params[:created_at] = Time.zone.parse(@opts[:created_at].to_s) if @opts[:created_at].present?

View File

@ -239,9 +239,9 @@ class TopicQuery
unless @user && @user.moderator? unless @user && @user.moderator?
category_ids = @user.secure_category_ids if @user category_ids = @user.secure_category_ids if @user
if category_ids.present? if category_ids.present?
result = result.where('categories.secure IS NULL OR categories.secure = ? OR categories.id IN (?)', false, category_ids) result = result.where('categories.read_restricted IS NULL OR categories.read_restricted = ? OR categories.id IN (?)', false, category_ids)
else else
result = result.where('categories.secure IS NULL OR categories.secure = ?', false) result = result.where('categories.read_restricted IS NULL OR categories.read_restricted = ?', false)
end end
end end

129
script/require_profiler.rb Normal file
View File

@ -0,0 +1,129 @@
# Some based on : https://gist.github.com/277289
#
# This is a rudimentary script that allows us to
# quickly determine if any gems are slowing down startup
require 'benchmark'
require 'fileutils'
module RequireProfiler
class << self
attr_accessor :stats
def profiling_enabled?
@profiling_enabled
end
def profile
start
yield
stop
end
def start(tmp_options={})
@start_time = Time.now
[ ::Kernel, (class << ::Kernel; self; end) ].each do |klass|
klass.class_eval do
def require_with_profiling(path, *args)
RequireProfiler.measure(path, caller, :require) { require_without_profiling(path, *args) }
end
alias require_without_profiling require
alias require require_with_profiling
def load_with_profiling(path, *args)
RequireProfiler.measure(path, caller, :load) { load_without_profiling(path, *args) }
end
alias load_without_profiling load
alias load load_with_profiling
end
end
# This is necessary so we don't clobber Bundler.require on Rails 3
Kernel.class_eval { private :require, :load }
@profiling_enabled = true
end
def stop
@stop_time = Time.now
[ ::Kernel, (class << ::Kernel; self; end) ].each do |klass|
klass.class_eval do
alias require require_without_profiling
alias load load_without_profiling
end
end
@profiling_enabled = false
end
def measure(path, full_backtrace, mechanism, &block)
# Path may be a Pathname, convert to a String
path = path.to_s
@stack ||= []
self.stats ||= {}
stat = self.stats.fetch(path){|key| self.stats[key] = {calls: 0, time: 0, parent_time: 0} }
@stack << stat
time = Time.now
begin
output = yield # do the require or load here
ensure
delta = Time.now - time
stat[:time] += delta
stat[:calls] += 1
@stack.pop
@stack.each do |frame|
frame[:parent_time] += delta
end
end
output
end
def time_block
start = Time.now
yield
Time.now - start
end
def gc_analyze
ObjectSpace.garbage_collect
gc_duration_start = time_block { ObjectSpace.garbage_collect }
old_objs = ObjectSpace.count_objects
yield
ObjectSpace.garbage_collect
gc_duration_finish = time_block { ObjectSpace.garbage_collect }
new_objs = ObjectSpace.count_objects
puts "New objects: #{(new_objs[:TOTAL] - new_objs[:FREE]) - (old_objs[:TOTAL] - old_objs[:FREE])}"
puts "GC duration: #{gc_duration_finish}"
puts "GC impact: #{gc_duration_finish - gc_duration_start}"
end
end
end
# RequireProfiler.gc_analyze do
# # require 'mime-types'
# require 'highline'
# end
# exit
RequireProfiler.profile do
Bundler.definition.dependencies.each do |dep|
begin
require dep.name unless dep.name =~ /timecop/
rescue Exception
# don't care
end
end
end
sorted = RequireProfiler.stats.to_a.sort{|a,b| b[1][:time] - b[1][:parent_time] <=> a[1][:time] - a[1][:parent_time]}
sorted[0..120].each do |k, v|
puts "#{k} : time #{v[:time] - v[:parent_time]} "
end

View File

@ -41,8 +41,7 @@ describe CategoryList do
cat = Fabricate(:category) cat = Fabricate(:category)
topic = Fabricate(:topic, category: cat) topic = Fabricate(:topic, category: cat)
cat.deny(:all) cat.set_permissions(:admins => :full)
cat.allow(Group[:admins])
cat.save cat.save
CategoryList.new(Guardian.new admin).categories.count.should == 1 CategoryList.new(Guardian.new admin).categories.count.should == 1

View File

@ -215,8 +215,9 @@ describe Guardian do
it 'correctly handles groups' do it 'correctly handles groups' do
group = Fabricate(:group) group = Fabricate(:group)
category = Fabricate(:category, secure: true) category = Fabricate(:category, read_restricted: true)
category.allow(group) category.set_permissions(group => :full)
category.save
topic = Fabricate(:topic, category: category) topic = Fabricate(:topic, category: category)
@ -275,8 +276,27 @@ describe Guardian do
end end
end end
describe 'a Topic' do
it 'should check for full permissions' do
category = Fabricate(:category)
category.set_permissions(:everyone => :create_post)
category.save
Guardian.new(user).can_create?(Topic,category).should be_false
end
end
describe 'a Post' do describe 'a Post' do
it "is false on readonly categories" do
category = Fabricate(:category)
topic.category = category
category.set_permissions(:everyone => :readonly)
category.save
Guardian.new(topic.user).can_create?(Post, topic).should be_false
end
it "is false when not logged in" do it "is false when not logged in" do
Guardian.new.can_create?(Post, topic).should be_false Guardian.new.can_create?(Post, topic).should be_false
end end

View File

@ -62,8 +62,7 @@ describe PostCreator do
admin = Fabricate(:admin) admin = Fabricate(:admin)
cat = Fabricate(:category) cat = Fabricate(:category)
cat.deny(:all) cat.set_permissions(:admins => :full)
cat.allow(Group[:admins])
cat.save cat.save
created_post = nil created_post = nil

View File

@ -15,15 +15,15 @@ test
end end
it "produces a quote even with new lines in it" do it "produces a quote even with new lines in it" do
PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd\n[/quote]").should match_html "<p></p><aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n <div class=\"quote-controls\"></div>\n <img width=\"20\" height=\"20\" src=\"/users/eviltrout/avatar/40?__ws=http%3A%2F%2Ftest.localhost\" class=\"avatar \" title=\"\">\n EvilTrout\n said:\n </div>\n <blockquote>ddd</blockquote>\n</aside><p></p>" PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd\n[/quote]").should match_html "<p></p><aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n <div class=\"quote-controls\"></div>\n <img width=\"20\" height=\"20\" src=\"/users/eviltrout/avatar/40?__ws=http%3A%2F%2Ftest.localhost\" class=\"avatar \" title=\"\">\n EvilTrout said:\n </div>\n <blockquote>ddd</blockquote>\n</aside><p></p>"
end end
it "should produce a quote" do it "should produce a quote" do
PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd[/quote]").should match_html "<p></p><aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n <div class=\"quote-controls\"></div>\n <img width=\"20\" height=\"20\" src=\"/users/eviltrout/avatar/40?__ws=http%3A%2F%2Ftest.localhost\" class=\"avatar \" title=\"\">\n EvilTrout\n said:\n </div>\n <blockquote>ddd</blockquote>\n</aside><p></p>" PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd[/quote]").should match_html "<p></p><aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n <div class=\"quote-controls\"></div>\n <img width=\"20\" height=\"20\" src=\"/users/eviltrout/avatar/40?__ws=http%3A%2F%2Ftest.localhost\" class=\"avatar \" title=\"\">\n EvilTrout said:\n </div>\n <blockquote>ddd</blockquote>\n</aside><p></p>"
end end
it "trims spaces on quote params" do it "trims spaces on quote params" do
PrettyText.cook("[quote=\"EvilTrout, post:555, topic: 666\"]ddd[/quote]").should match_html "<p></p><aside class=\"quote\" data-post=\"555\" data-topic=\"666\"><div class=\"title\">\n <div class=\"quote-controls\"></div>\n <img width=\"20\" height=\"20\" src=\"/users/eviltrout/avatar/40?__ws=http%3A%2F%2Ftest.localhost\" class=\"avatar \" title=\"\">\n EvilTrout\n said:\n </div>\n <blockquote>ddd</blockquote>\n</aside><p></p>" PrettyText.cook("[quote=\"EvilTrout, post:555, topic: 666\"]ddd[/quote]").should match_html "<p></p><aside class=\"quote\" data-post=\"555\" data-topic=\"666\"><div class=\"title\">\n <div class=\"quote-controls\"></div>\n <img width=\"20\" height=\"20\" src=\"/users/eviltrout/avatar/40?__ws=http%3A%2F%2Ftest.localhost\" class=\"avatar \" title=\"\">\n EvilTrout said:\n </div>\n <blockquote>ddd</blockquote>\n</aside><p></p>"
end end

View File

@ -171,8 +171,7 @@ describe Search do
topic.category_id = category.id topic.category_id = category.id
topic.save topic.save
category.deny(:all) category.set_permissions(:staff => :full)
category.allow(Group[:staff])
category.save category.save
result(nil).should_not be_present result(nil).should_not be_present
@ -211,7 +210,7 @@ describe Search do
r[:title].should == category.name r[:title].should == category.name
r[:url].should == "/category/#{category.slug}" r[:url].should == "/category/#{category.slug}"
category.deny(:all) category.set_permissions({})
category.save category.save
result.should_not be_present result.should_not be_present

View File

@ -14,9 +14,8 @@ describe TopicQuery do
context 'secure category' do context 'secure category' do
it "filters categories out correctly" do it "filters categories out correctly" do
category = Fabricate(:category) category = Fabricate(:category)
category.deny(:all)
group = Fabricate(:group) group = Fabricate(:group)
category.allow(group) category.set_permissions(group => :full)
category.save category.save
topic = Fabricate(:topic, category: category) topic = Fabricate(:topic, category: category)

View File

@ -1,6 +1,7 @@
require 'spec_helper' require 'spec_helper'
describe Admin::GroupsController do describe Admin::GroupsController do
it "is a subclass of AdminController" do it "is a subclass of AdminController" do
(Admin::GroupsController < Admin::AdminController).should be_true (Admin::GroupsController < Admin::AdminController).should be_true
end end
@ -13,7 +14,7 @@ describe Admin::GroupsController do
xhr :get, :index xhr :get, :index
response.status.should == 200 response.status.should == 200
::JSON.parse(response.body).should == [{ ::JSON.parse(response.body).keep_if{|r| r["id"] == group.id}.should == [{
"id"=>group.id, "id"=>group.id,
"name"=>group.name, "name"=>group.name,
"user_count"=>1, "user_count"=>1,
@ -36,7 +37,7 @@ describe Admin::GroupsController do
xhr :delete, :destroy, id: group.id xhr :delete, :destroy, id: group.id
response.status.should == 200 response.status.should == 200
Group.count.should == 0 Group.where(id: group.id).count.should == 0
end end
it "is able to create a group" do it "is able to create a group" do
@ -49,7 +50,7 @@ describe Admin::GroupsController do
response.status.should == 200 response.status.should == 200
groups = Group.all.to_a groups = Group.where(name: "bob").to_a
groups.count.should == 1 groups.count.should == 1
groups[0].usernames.should == a.username groups[0].usernames.should == a.username

View File

@ -1,13 +1,13 @@
require 'spec_helper' require "spec_helper"
describe CategoriesController do describe CategoriesController do
describe 'create' do describe "create" do
it 'requires the user to be logged in' do it "requires the user to be logged in" do
lambda { xhr :post, :create }.should raise_error(Discourse::NotLoggedIn) lambda { xhr :post, :create }.should raise_error(Discourse::NotLoggedIn)
end end
describe 'logged in' do describe "logged in" do
before do before do
@user = log_in(:moderator) @user = log_in(:moderator)
end end
@ -18,55 +18,66 @@ describe CategoriesController do
response.should be_forbidden response.should be_forbidden
end end
it 'raises an exception when the name is missing' do it "raises an exception when the name is missing" do
lambda { xhr :post, :create, color: 'ff0', text_color: 'fff' }.should raise_error(ActionController::ParameterMissing) lambda { xhr :post, :create, color: "ff0", text_color: "fff" }.should raise_error(ActionController::ParameterMissing)
end end
it 'raises an exception when the color is missing' do it "raises an exception when the color is missing" do
lambda { xhr :post, :create, name: 'hello', text_color: 'fff' }.should raise_error(ActionController::ParameterMissing) lambda { xhr :post, :create, name: "hello", text_color: "fff" }.should raise_error(ActionController::ParameterMissing)
end end
it 'raises an exception when the text color is missing' do it "raises an exception when the text color is missing" do
lambda { xhr :post, :create, name: 'hello', color: 'ff0' }.should raise_error(ActionController::ParameterMissing) lambda { xhr :post, :create, name: "hello", color: "ff0" }.should raise_error(ActionController::ParameterMissing)
end end
describe 'failure' do describe "failure" do
before do before do
@category = Fabricate(:category, user: @user) @category = Fabricate(:category, user: @user)
xhr :post, :create, name: @category.name, color: 'ff0', text_color: 'fff' xhr :post, :create, name: @category.name, color: "ff0", text_color: "fff"
end end
it { should_not respond_with(:success) } it { should_not respond_with(:success) }
it 'returns errors on a duplicate category name' do it "returns errors on a duplicate category name" do
response.code.to_i.should == 422 response.status.should == 422
end end
end end
describe 'success' do describe "success" do
before do it "works" do
xhr :post, :create, name: 'hello', color: 'ff0', text_color: 'fff' readonly = CategoryGroup.permission_types[:readonly]
create_post = CategoryGroup.permission_types[:create_post]
xhr :post, :create, name: "hello", color: "ff0", text_color: "fff",
hotness: 2,
auto_close_days: 3,
permissions: {
"everyone" => readonly,
"staff" => create_post
}
response.status.should == 200
category = Category.first
category.category_groups.map{|g| [g.group_id, g.permission_type]}.sort.should == [
[Group[:everyone].id, readonly],[Group[:staff].id,create_post]
]
category.name.should == "hello"
category.color.should == "ff0"
category.hotness.should == 2
category.auto_close_days.should == 3
end end
it 'creates a category' do
Category.count.should == 1
end
it { should respond_with(:success) }
end end
end end
end end
describe 'destroy' do describe "destroy" do
it 'requires the user to be logged in' do it "requires the user to be logged in" do
lambda { xhr :delete, :destroy, id: 'category'}.should raise_error(Discourse::NotLoggedIn) lambda { xhr :delete, :destroy, id: "category"}.should raise_error(Discourse::NotLoggedIn)
end end
describe 'logged in' do describe "logged in" do
before do before do
@user = log_in @user = log_in
@category = Fabricate(:category, user: @user) @category = Fabricate(:category, user: @user)
@ -86,14 +97,14 @@ describe CategoriesController do
end end
describe 'update' do describe "update" do
it 'requires the user to be logged in' do it "requires the user to be logged in" do
lambda { xhr :put, :update, id: 'category'}.should raise_error(Discourse::NotLoggedIn) lambda { xhr :put, :update, id: 'category'}.should raise_error(Discourse::NotLoggedIn)
end end
describe 'logged in' do describe "logged in" do
before do before do
@user = log_in(:moderator) @user = log_in(:moderator)
@category = Fabricate(:category, user: @user) @category = Fabricate(:category, user: @user)
@ -117,37 +128,46 @@ describe CategoriesController do
lambda { xhr :put, :update, id: @category.slug, name: 'asdf', color: 'fff' }.should raise_error(ActionController::ParameterMissing) lambda { xhr :put, :update, id: @category.slug, name: 'asdf', color: 'fff' }.should raise_error(ActionController::ParameterMissing)
end end
describe 'failure' do describe "failure" do
before do before do
@other_category = Fabricate(:category, name: 'Other', user: @user ) @other_category = Fabricate(:category, name: "Other", user: @user )
xhr :put, :update, id: @category.id, name: @other_category.name, color: 'ff0', text_color: 'fff' xhr :put, :update, id: @category.id, name: @other_category.name, color: "ff0", text_color: "fff"
end end
it 'returns errors on a duplicate category name' do it "returns errors on a duplicate category name" do
response.should_not be_success response.should_not be_success
end end
it 'returns errors on a duplicate category name' do it "returns errors on a duplicate category name" do
response.code.to_i.should == 422 response.code.to_i.should == 422
end end
end end
describe 'success' do describe "success" do
before do
# might as well test this as well
@category.allow(Group[:admins])
@category.save
xhr :put, :update, id: @category.id, name: 'science', color: '000', text_color: '0ff', group_names: Group[:staff].name, secure: 'true' it "updates the group correctly" do
readonly = CategoryGroup.permission_types[:readonly]
create_post = CategoryGroup.permission_types[:create_post]
xhr :put, :update, id: @category.id, name: "hello", color: "ff0", text_color: "fff",
hotness: 2,
auto_close_days: 3,
permissions: {
"everyone" => readonly,
"staff" => create_post
}
response.status.should == 200
@category.reload @category.reload
end @category.category_groups.map{|g| [g.group_id, g.permission_type]}.sort.should == [
[Group[:everyone].id, readonly],[Group[:staff].id,create_post]
]
@category.name.should == "hello"
@category.color.should == "ff0"
@category.hotness.should == 2
@category.auto_close_days.should == 3
it 'updates the group correctly' do
@category.name.should == 'science'
@category.color.should == '000'
@category.text_color.should == '0ff'
@category.secure?.should be_true
@category.groups.count.should == 1
end end
end end
end end

View File

@ -15,8 +15,7 @@ describe CategoryFeaturedTopic do
# so much dancing, I am thinking fixures make sense here. # so much dancing, I am thinking fixures make sense here.
user.change_trust_level!(:basic) user.change_trust_level!(:basic)
category.deny(:all) category.set_permissions(:trust_level_1 => :full)
category.allow(Group[:trust_level_1])
category.save category.save
uncategorized_post = PostCreator.create(user, raw: "this is my new post 123 post", title: "hello world") uncategorized_post = PostCreator.create(user, raw: "this is my new post 123 post", title: "hello world")

View File

@ -18,6 +18,63 @@ describe Category do
it { should have_many :category_featured_topics } it { should have_many :category_featured_topics }
it { should have_many :featured_topics } it { should have_many :featured_topics }
describe "resolve_permissions" do
it "can determine read_restricted" do
read_restricted, resolved = Category.resolve_permissions(:everyone => :full)
read_restricted.should be_false
resolved.should == []
end
end
describe "topic_create_allowed and post_create_allowed" do
it "works" do
default_category = Fabricate(:category)
full_category = Fabricate(:category)
can_post_category = Fabricate(:category)
can_read_category = Fabricate(:category)
user = Fabricate(:user)
group = Fabricate(:group)
group.add(user)
group.save
admin = Fabricate(:admin)
full_category.set_permissions(group => :full)
full_category.save
can_post_category.set_permissions(group => :create_post)
can_post_category.save
can_read_category.set_permissions(group => :readonly)
can_read_category.save
guardian = Guardian.new(admin)
Category.topic_create_allowed(guardian).count.should == 4
Category.post_create_allowed(guardian).count.should == 4
Category.secured(guardian).count.should == 4
guardian = Guardian.new(user)
Category.secured(guardian).count.should == 4
Category.post_create_allowed(guardian).count.should == 3
Category.topic_create_allowed(guardian).count.should == 2 # explicitly allowed once, default allowed once
# everyone has special semantics, test it as well
can_post_category.set_permissions(:everyone => :create_post)
can_post_category.save
Category.post_create_allowed(guardian).count.should == 3
end
end
describe "post_create_allowed" do
end
describe "security" do describe "security" do
let(:category) { Fabricate(:category) } let(:category) { Fabricate(:category) }
let(:category_2) { Fabricate(:category) } let(:category_2) { Fabricate(:category) }
@ -25,20 +82,20 @@ describe Category do
let(:group) { Fabricate(:group) } let(:group) { Fabricate(:group) }
it "secures categories correctly" do it "secures categories correctly" do
category.secure?.should be_false category.read_restricted?.should be_false
category.deny(:all) category.set_permissions({})
category.secure?.should be_true category.read_restricted?.should be_true
category.allow(:all) category.set_permissions(:everyone => :full)
category.secure?.should be_false category.read_restricted?.should be_false
user.secure_categories.should be_empty user.secure_categories.should be_empty
group.add(user) group.add(user)
group.save group.save
category.allow(group) category.set_permissions(group.id => :full)
category.save category.save
user.reload user.reload
@ -47,13 +104,13 @@ describe Category do
it "lists all secured categories correctly" do it "lists all secured categories correctly" do
group.add(user) group.add(user)
category.allow(group) category.set_permissions(group.id => :full)
category.save
category_2.set_permissions(group.id => :full)
category_2.save
Category.secured.should == [category] Category.secured.should =~ []
Category.secured(Guardian.new(user)).should =~ [category, category_2]
category_2.allow(group)
Category.secured.should =~ [category, category_2]
end end
end end

View File

@ -80,6 +80,11 @@ describe PostAnalyzer do
post_analyzer = PostAnalyzer.new(raw_three_links, default_topic_id) post_analyzer = PostAnalyzer.new(raw_three_links, default_topic_id)
post_analyzer.linked_hosts.should == {"discourse.org" => 1, "www.imdb.com" => 1} post_analyzer.linked_hosts.should == {"discourse.org" => 1, "www.imdb.com" => 1}
end end
it 'returns blank for ipv6 output' do
post_analyzer = PostAnalyzer.new('PING www.google.com(lb-in-x93.1e100.net) 56 data bytes', default_topic_id)
post_analyzer.linked_hosts.should be_blank
end
end end
end end

17
spec/models/site_spec.rb Normal file
View File

@ -0,0 +1,17 @@
require 'spec_helper'
require_dependency 'site'
describe Site do
it "omits categories users can not write to from the category list" do
category = Fabricate(:category)
user = Fabricate(:user)
Site.new(Guardian.new(user)).categories.count.should == 1
category.set_permissions(:everyone => :create_post)
category.save
# TODO clean up querying so we can make sure we have the correct permission set
Site.new(Guardian.new(user)).categories[0].permission.should_not == CategoryGroup.permission_types[:full]
end
end

View File

@ -258,8 +258,7 @@ describe TopicLink do
TopicLink.topic_summary(Guardian.new, post.topic_id).count.should == 1 TopicLink.topic_summary(Guardian.new, post.topic_id).count.should == 1
TopicLink.counts_for(Guardian.new, post.topic, [post]).length.should == 1 TopicLink.counts_for(Guardian.new, post.topic, [post]).length.should == 1
category.deny(:all) category.set_permissions(:staff => :full)
category.allow(Group[:staff])
category.save category.save
admin = Fabricate(:admin) admin = Fabricate(:admin)

View File

@ -192,7 +192,7 @@ describe Topic do
context "secure categories" do context "secure categories" do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
let(:category) { Fabricate(:category, secure: true) } let(:category) { Fabricate(:category, read_restricted: true) }
before do before do
topic.category = category topic.category = category
@ -1263,7 +1263,7 @@ describe Topic do
describe 'secured' do describe 'secured' do
it 'can remove secure groups' do it 'can remove secure groups' do
category = Fabricate(:category, secure: true) category = Fabricate(:category, read_restricted: true)
topic = Fabricate(:topic, category: category) topic = Fabricate(:topic, category: category)
Topic.secured(Guardian.new(nil)).count.should == 0 Topic.secured(Guardian.new(nil)).count.should == 0
@ -1280,17 +1280,17 @@ describe Topic do
let(:category){ Category.new } let(:category){ Category.new }
it "is true if the category is secure" do it "is true if the category is secure" do
category.stubs(:secure).returns(true) category.stubs(:read_restricted).returns(true)
Topic.new(:category => category).should be_secure_category Topic.new(:category => category).should be_read_restricted_category
end end
it "is false if the category is not secure" do it "is false if the category is not secure" do
category.stubs(:secure).returns(false) category.stubs(:read_restricted).returns(false)
Topic.new(:category => category).should_not be_secure_category Topic.new(:category => category).should_not be_read_restricted_category
end end
it "is false if there is no category" do it "is false if there is no category" do
Topic.new(:category => nil).should_not be_secure_category Topic.new(:category => nil).should_not be_read_restricted_category
end end
end end

View File

@ -51,7 +51,7 @@ describe TopicTrackingState do
row.user_id.should == post.user_id row.user_id.should == post.user_id
# when we have no permission to see a category, don't show its stats # when we have no permission to see a category, don't show its stats
category = Fabricate(:category, secure: true) category = Fabricate(:category, read_restricted: true)
post.topic.category_id = category.id post.topic.category_id = category.id
post.topic.save post.topic.save

View File

@ -68,7 +68,7 @@ describe UserAction do
# groups # groups
category = Fabricate(:category, secure: true) category = Fabricate(:category, read_restricted: true)
public_topic.recover! public_topic.recover!
public_topic.category = category public_topic.category = category
@ -82,7 +82,7 @@ describe UserAction do
group.add(u) group.add(u)
group.save group.save
category.allow(group) category.set_permissions(group => :full)
category.save category.save
stats_for_user(u).should == [UserAction::NEW_TOPIC] stats_for_user(u).should == [UserAction::NEW_TOPIC]

View File

@ -77,21 +77,21 @@ test("quote formatting", function() {
// TODO: This HTML matching is quite ugly. // TODO: This HTML matching is quite ugly.
format("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]", format("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]",
"</p><aside class='quote' data-post=\"1\" data-topic=\"1\" >\n <div class='title'>\n " + "</p><aside class='quote' data-post=\"1\" data-topic=\"1\" >\n <div class='title'>\n " +
"<div class='quote-controls'></div>\n \n eviltrout\n said:\n </div>\n <blockquote>abc</blockquote>\n</aside>\n<p>", "<div class='quote-controls'></div>\n \n eviltrout said:\n </div>\n <blockquote>abc</blockquote>\n</aside>\n<p>",
"renders quotes properly"); "renders quotes properly");
format("[quote=\"eviltrout, post:1, topic:1\"]abc[quote=\"eviltrout, post:2, topic:2\"]nested[/quote][/quote]", format("[quote=\"eviltrout, post:1, topic:1\"]abc[quote=\"eviltrout, post:2, topic:2\"]nested[/quote][/quote]",
"</p><aside class='quote' data-post=\"1\" data-topic=\"1\" >\n <div class='title'>\n <div " + "</p><aside class='quote' data-post=\"1\" data-topic=\"1\" >\n <div class='title'>\n <div " +
"class='quote-controls'></div>\n \n eviltrout\n said:\n </div>\n <blockquote>abc</p><aside " + "class='quote-controls'></div>\n \n eviltrout said:\n </div>\n <blockquote>abc</p><aside " +
"class='quote' data-post=\"2\" data-topic=\"2\" >\n <div class='title'>\n <div class='quote-" + "class='quote' data-post=\"2\" data-topic=\"2\" >\n <div class='title'>\n <div class='quote-" +
"controls'></div>\n \n eviltrout\n said:\n </div>\n <blockquote>nested</blockquote>\n</aside>\n<p></blockquote>\n</aside>\n<p>", "controls'></div>\n \n eviltrout said:\n </div>\n <blockquote>nested</blockquote>\n</aside>\n<p></blockquote>\n</aside>\n<p>",
"can nest quotes"); "can nest quotes");
format("before[quote=\"eviltrout, post:1, topic:1\"]first[/quote]middle[quote=\"eviltrout, post:2, topic:2\"]second[/quote]after", format("before[quote=\"eviltrout, post:1, topic:1\"]first[/quote]middle[quote=\"eviltrout, post:2, topic:2\"]second[/quote]after",
"before</p><aside class='quote' data-post=\"1\" data-topic=\"1\" >\n <div class='title'>\n <div class='quote-cont" + "before</p><aside class='quote' data-post=\"1\" data-topic=\"1\" >\n <div class='title'>\n <div class='quote-cont" +
"rols'></div>\n \n eviltrout\n said:\n </div>\n <blockquote>first</blockquote>\n</aside>\n<p>middle</p><aside cla" + "rols'></div>\n \n eviltrout said:\n </div>\n <blockquote>first</blockquote>\n</aside>\n<p>middle</p><aside cla" +
"ss='quote' data-post=\"2\" data-topic=\"2\" >\n <div class='title'>\n <div class='quote-controls'></div>\n \n " + "ss='quote' data-post=\"2\" data-topic=\"2\" >\n <div class='title'>\n <div class='quote-controls'></div>\n \n " +
"eviltrout\n said:\n </div>\n <blockquote>second</blockquote>\n</aside>\n<p>after", "eviltrout said:\n </div>\n <blockquote>second</blockquote>\n</aside>\n<p>after",
"can handle more than one quote"); "can handle more than one quote");
}); });

View File

@ -69,13 +69,13 @@ test("Quotes", function() {
cookedOptions("1[quote=\"bob, post:1\"]my quote[/quote]2", cookedOptions("1[quote=\"bob, post:1\"]my quote[/quote]2",
{ topicId: 2, lookupAvatar: function(name) { return "" + name; } }, { topicId: 2, lookupAvatar: function(name) { return "" + name; } },
"<p>1</p><aside class='quote' data-post=\"1\" >\n <div class='title'>\n <div class='quote-controls'></div>\n" + "<p>1</p><aside class='quote' data-post=\"1\" >\n <div class='title'>\n <div class='quote-controls'></div>\n" +
" bob\n bob\n said:\n </div>\n <blockquote>my quote</blockquote>\n</aside>\n<p></p>\n\n<p>2</p>", " bob\n bob said:\n </div>\n <blockquote>my quote</blockquote>\n</aside>\n<p></p>\n\n<p>2</p>",
"handles quotes properly"); "handles quotes properly");
cookedOptions("1[quote=\"bob, post:1\"]my quote[/quote]2", cookedOptions("1[quote=\"bob, post:1\"]my quote[/quote]2",
{ topicId: 2, lookupAvatar: function(name) { } }, { topicId: 2, lookupAvatar: function(name) { } },
"<p>1</p><aside class='quote' data-post=\"1\" >\n <div class='title'>\n <div class='quote-controls'></div>\n" + "<p>1</p><aside class='quote' data-post=\"1\" >\n <div class='title'>\n <div class='quote-controls'></div>\n" +
" \n bob\n said:\n </div>\n <blockquote>my quote</blockquote>\n</aside>\n<p></p>\n\n<p>2</p>", " \n bob said:\n </div>\n <blockquote>my quote</blockquote>\n</aside>\n<p></p>\n\n<p>2</p>",
"includes no avatar if none is found"); "includes no avatar if none is found");
}); });

View File

@ -4,17 +4,20 @@ module("Discourse.Onebox", {
} }
}); });
test("Stops rapid calls with cache true", function() { asyncTestDiscourse("Stops rapid calls with cache true", function() {
this.stub(Discourse, "ajax").returns(resolvingPromise); this.stub(Discourse, "ajax").returns(Ember.RSVP.resolve());
Discourse.Onebox.load(this.anchor, true);
Discourse.Onebox.load(this.anchor, true);
Discourse.Onebox.load(this.anchor, true); start();
Discourse.Onebox.load(this.anchor, true);
ok(Discourse.ajax.calledOnce); ok(Discourse.ajax.calledOnce);
}); });
test("Stops rapid calls with cache false", function() { asyncTestDiscourse("Stops rapid calls with cache true", function() {
this.stub(Discourse, "ajax").returns(resolvingPromise); this.stub(Discourse, "ajax").returns(Ember.RSVP.resolve());
Discourse.Onebox.load(this.anchor, false); Discourse.Onebox.load(this.anchor, false);
Discourse.Onebox.load(this.anchor, false); Discourse.Onebox.load(this.anchor, false);
start();
ok(Discourse.ajax.calledOnce); ok(Discourse.ajax.calledOnce);
}); });

View File

@ -14,7 +14,7 @@ test("remove", function() {
blank(PreloadStore.get('bane'), "removes the value if the key exists"); blank(PreloadStore.get('bane'), "removes the value if the key exists");
}); });
asyncTest("getAndRemove returns a promise that resolves to null", function() { asyncTestDiscourse("getAndRemove returns a promise that resolves to null", function() {
expect(1); expect(1);
PreloadStore.getAndRemove('joker').then(function(result) { PreloadStore.getAndRemove('joker').then(function(result) {
@ -23,7 +23,7 @@ asyncTest("getAndRemove returns a promise that resolves to null", function() {
}); });
}); });
asyncTest("getAndRemove returns a promise that resolves to the result of the finder", function() { asyncTestDiscourse("getAndRemove returns a promise that resolves to the result of the finder", function() {
expect(1); expect(1);
var finder = function() { return 'batdance'; }; var finder = function() { return 'batdance'; };
@ -34,7 +34,7 @@ asyncTest("getAndRemove returns a promise that resolves to the result of the fin
}); });
asyncTest("getAndRemove returns a promise that resolves to the result of the finder's promise", function() { asyncTestDiscourse("getAndRemove returns a promise that resolves to the result of the finder's promise", function() {
expect(1); expect(1);
var finder = function() { var finder = function() {
@ -47,7 +47,7 @@ asyncTest("getAndRemove returns a promise that resolves to the result of the fin
}); });
}); });
asyncTest("returns a promise that rejects with the result of the finder's rejected promise", function() { asyncTestDiscourse("returns a promise that rejects with the result of the finder's rejected promise", function() {
expect(1); expect(1);
var finder = function() { var finder = function() {
@ -61,7 +61,7 @@ asyncTest("returns a promise that rejects with the result of the finder's reject
}); });
asyncTest("returns a promise that resolves to 'evil'", function() { asyncTestDiscourse("returns a promise that resolves to 'evil'", function() {
expect(1); expect(1);
PreloadStore.getAndRemove('bane').then(function(result) { PreloadStore.getAndRemove('bane').then(function(result) {

View File

@ -1,11 +1,11 @@
// Test helpers // Test helpers
var resolvingPromise = Ember.Deferred.promise(function (p) { // var resolvingPromise = Ember.Deferred.promise(function (p) {
p.resolve(); // p.resolve();
}); // });
var resolvingPromiseWith = function(result) { // var resolvingPromiseWith = function(result) {
return Ember.Deferred.promise(function (p) { p.resolve(result); }); // return Ember.Deferred.promise(function (p) { p.resolve(result); });
}; // };
function exists(selector) { function exists(selector) {
return !!count(selector); return !!count(selector);

View File

@ -18,4 +18,15 @@ function controllerFor(controller, model) {
var controller = Discourse.__container__.lookup('controller:' + controller); var controller = Discourse.__container__.lookup('controller:' + controller);
if (model) { controller.set('model', model ); } if (model) { controller.set('model', model ); }
return controller; return controller;
}
function asyncTestDiscourse(text, func) {
asyncTest(text, function () {
var qunitContext = this;
Ember.run(function () {
func.call(qunitContext);
});
});
} }

View File

@ -8,7 +8,7 @@ var qHint = function(name, sourceFile, options, globals) {
sourceFile = name; sourceFile = name;
} }
return asyncTest(name, function() { return asyncTestDiscourse(name, function() {
qHint.sendRequest(sourceFile, function(req) { qHint.sendRequest(sourceFile, function(req) {
start(); start();
@ -113,9 +113,8 @@ var jsHintOpts = {
"visit", "visit",
"count", "count",
"exists", "exists",
"asyncTest", "asyncTestDiscourse",
"find", "find",
"resolvingPromise",
"sinon", "sinon",
"moment", "moment",
"start", "start",
@ -125,7 +124,6 @@ var jsHintOpts = {
"controllerFor", "controllerFor",
"containsInstance", "containsInstance",
"deepEqual", "deepEqual",
"resolvingPromiseWith",
"Blob", "Blob",
"File"], "File"],
"node" : false, "node" : false,

View File

@ -130,7 +130,7 @@ test('editingFirstPost', function() {
}); });
asyncTest('importQuote with a post', function() { asyncTestDiscourse('importQuote with a post', function() {
expect(1); expect(1);
this.stub(Discourse.Post, 'load').withArgs(123).returns(Em.Deferred.promise(function (p) { this.stub(Discourse.Post, 'load').withArgs(123).returns(Em.Deferred.promise(function (p) {
@ -144,7 +144,7 @@ asyncTest('importQuote with a post', function() {
}); });
}); });
asyncTest('importQuote with no post', function() { asyncTestDiscourse('importQuote with no post', function() {
expect(1); expect(1);
this.stub(Discourse.Post, 'load').withArgs(4).returns(Em.Deferred.promise(function (p) { this.stub(Discourse.Post, 'load').withArgs(4).returns(Em.Deferred.promise(function (p) {

View File

@ -213,7 +213,7 @@ test("identity map", function() {
deepEqual(postStream.listUnloadedIds([1, 2, 3, 4]), [2, 4], "it only returns unloaded posts"); deepEqual(postStream.listUnloadedIds([1, 2, 3, 4]), [2, 4], "it only returns unloaded posts");
}); });
asyncTest("loadIntoIdentityMap with no data", function() { asyncTestDiscourse("loadIntoIdentityMap with no data", function() {
var postStream = buildStream(1234); var postStream = buildStream(1234);
expect(1); expect(1);
@ -224,11 +224,11 @@ asyncTest("loadIntoIdentityMap with no data", function() {
}); });
}); });
asyncTest("loadIntoIdentityMap with post ids", function() { asyncTestDiscourse("loadIntoIdentityMap with post ids", function() {
var postStream = buildStream(1234); var postStream = buildStream(1234);
expect(1); expect(1);
this.stub(Discourse, "ajax").returns(resolvingPromiseWith({ this.stub(Discourse, "ajax").returns(Ember.RSVP.resolve({
post_stream: { post_stream: {
posts: [{id: 10, post_number: 10}] posts: [{id: 10, post_number: 10}]
} }

View File

@ -11,7 +11,7 @@
// Externals we need to load first // Externals we need to load first
//= require ../../app/assets/javascripts/external/jquery-1.9.1.js //= require ../../app/assets/javascripts/external/jquery-1.9.1.js
//= require ../../app/assets/javascripts/external/jquery.ui.widget.js //= require ../../app/assets/javascripts/external/jquery.ui.widget.js
//= require ../../app/assets/javascripts/external/handlebars-1.0.rc.4.js //= require ../../app/assets/javascripts/external/handlebars.js
//= require ../../app/assets/javascripts/external_development/ember.js //= require ../../app/assets/javascripts/external_development/ember.js
//= require ../../app/assets/javascripts/external_development/group-helper.js //= require ../../app/assets/javascripts/external_development/group-helper.js
@ -46,11 +46,11 @@
// sinon settings // sinon settings
sinon.config = { sinon.config = {
injectIntoThis: true, injectIntoThis: true,
injectInto: null, injectInto: null,
properties: ["spy", "stub", "mock", "clock", "sandbox"], properties: ["spy", "stub", "mock", "clock", "sandbox"],
useFakeTimers: false, useFakeTimers: false,
useFakeServer: false useFakeServer: false
}; };
window.assetPath = function() { return null }; window.assetPath = function() { return null };