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/redis.yml
config/discourse.pill
config/environments/production.rb
# Ignore the default SQLite database and db dumps
/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
# PR that makes it all hang together welcome
gem 'ember-rails'
gem 'ember-source', '1.0.0.rc5' # or the version you need
gem 'handlebars-source', '1.0.0.rc4' # or the version you need
gem 'ember-source', '1.0.0.rc6.2'
gem 'handlebars-source', '1.0.12'
gem 'barber'
gem 'vestal_versions', git: 'https://github.com/zhangyuan/vestal_versions'

View File

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

View File

@ -238,9 +238,9 @@ Discourse.AdminUser = Discourse.User.extend({
loadDetails: function() {
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.set('loadedDetails', true);
});

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,15 @@
<div class='admin-controls'>
<div class='span15'>
<ul class="nav nav-pills">
<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.active'}}{{i18n admin.users.nav.active}}{{/linkTo}}</li>
<li>{{#linkTo 'adminUsersList.new'}}{{i18n admin.users.nav.new}}{{/linkTo}}</li>
{{#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}}
<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.banned}}{{i18n admin.users.nav.banned}}{{/linkTo}}</li>
<li>{{#linkTo adminUsersList.blocked}}{{i18n admin.users.nav.blocked}}{{/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.banned'}}{{i18n admin.users.nav.banned}}{{/linkTo}}</li>
<li>{{#linkTo 'adminUsersList.blocked'}}{{i18n admin.users.nav.blocked}}{{/linkTo}}</li>
</ul>
</div>
<div class='span5 username controls'>

View File

@ -12,7 +12,7 @@
// Externals we need to load first
//= require ./external/jquery-1.9.1.js
//= require ./external/jquery.ui.widget.js
//= require ./external/handlebars-1.0.rc.4.js
//= require ./external/handlebars.js
<%
if Rails.env.development?
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)
var fixture = Discourse.URL_FIXTURES && Discourse.URL_FIXTURES[url];
if (fixture) {
return Ember.Deferred.promise(function(promise) {
promise.resolve(fixture);
});
return Ember.RSVP.resolve(fixture);
}
return Ember.Deferred.promise(function (promise) {

View File

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

View File

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

View File

@ -100,11 +100,16 @@ Discourse.QuoteButtonController = Discourse.Controller.extend({
var post = this.get('post');
var composerController = this.get('controllers.composer');
var composerOpts = {
post: post,
action: Discourse.Composer.REPLY,
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.
var composerPost = composerController.get('content.post');
if (composerPost && (composerPost.get('id') !== this.get('post.id'))) {

View File

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

View File

@ -198,41 +198,6 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
Discourse.URL.routeTo(this.get('lastPostUrl'));
},
replyAsNewTopic: function(post) {
// TODO shut down topic draft cleanly if it exists ...
var composerController = this.get('controllers.composer');
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
@ -336,25 +301,60 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
var composerController = this.get('controllers.composer');
var quoteController = this.get('controllers.quoteButton');
var quotedText = Discourse.BBCode.buildQuoteBBCode(quoteController.get('post'), quoteController.get('buffer'));
var topic = post ? post.get('topic') : this.get('model');
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.set('content.post', post);
composerController.set('content.composeState', Discourse.Composer.OPEN);
composerController.appendText(quotedText);
} else {
var promise = composerController.open({
post: post,
var opts = {
action: Discourse.Composer.REPLY,
draftKey: post.get('topic.draft_key'),
draftSequence: post.get('topic.draft_sequence')
});
draftKey: topic.get('draft_key'),
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); });
}
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
editPost: function(post) {
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() {
this._super();
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() {
return ({ type: 'category', id: this.get('id'), category: this });
}.property('id'),
@ -43,33 +55,49 @@ Discourse.Category = Discourse.Model.extend({
text_color: this.get('text_color'),
hotness: this.get('hotness'),
secure: this.get('secure'),
group_names: this.get('groups').join(","),
permissions: this.get('permissionsForUpdate'),
auto_close_days: this.get('auto_close_days')
},
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) {
return Discourse.ajax("/categories/" + (this.get('slug') || this.get('id')), { type: 'DELETE' });
},
addGroup: function(group){
this.get("groups").addObject(group);
this.get("availableGroups").removeObject(group);
addPermission: function(permission){
this.get("permissions").addObject(permission);
this.get("availableGroups").removeObject(permission.group_name);
},
removeGroup: function(group){
this.get("groups").removeObject(group);
this.get("availableGroups").addObject(group);
removePermission: function(permission){
this.get("permissions").removeObject(permission);
this.get("availableGroups").addObject(permission.group_name);
},
// note, this is used in a data attribute, data attributes get downcased
// to avoid confusion later on using this naming here.
description_text: function(){
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({
shareUrl: function() {
if (this.get('firstPost')) return this.get('topic.url');
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'),
new_user: Em.computed.equal('trust_level', 0),

View File

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

View File

@ -6,13 +6,16 @@
@namespace Discourse
@module Discourse
**/
Discourse.ListCategoriesRoute = Discourse.Route.extend(Discourse.ModelReady, {
Discourse.ListCategoriesRoute = Discourse.Route.extend({
redirect: function() { Discourse.redirectIfLoginRequired(this); },
events: {
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');
}
},
@ -28,9 +31,11 @@ Discourse.ListCategoriesRoute = Discourse.Route.extend(Discourse.ModelReady, {
this.controllerFor('list').set('canCreateCategory', false);
},
modelReady: function(controller, categoryList) {
renderTemplate: function() {
this.render('listCategories', { into: 'list', outlet: 'listView' });
},
afterModel: function(categoryList) {
this.controllerFor('list').setProperties({
canCreateCategory: categoryList.get('can_create_category'),
canCreateTopic: categoryList.get('can_create_topic'),

View File

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

View File

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

View File

@ -85,7 +85,7 @@
</li>
<li class='current-user'>
{{#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}}
<div class="icon not-logged-in-avatar" {{action showLogin}}><i class='icon-user'></i></div>
{{/if}}
@ -110,7 +110,7 @@
</section>
<section class='d-dropdown' id='site-map-dropdown'>
<ul>
<ul class="location-links">
{{#if currentUser.staff}}
<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>
@ -123,8 +123,10 @@
{{#titledLinkTo "list.latest" titleKey="filters.latest.help"}}{{i18n filters.latest.title}}{{/titledLinkTo}}
</li>
<li>{{faqLink}}</li>
</ul>
{{#if categories}}
{{#if categories}}
<ul class="category-links">
<li class='heading' title="{{i18n filters.categories.help}}">
{{#linkTo "list.categories"}}{{i18n filters.categories.title}}{{/linkTo}}
</li>
@ -135,9 +137,9 @@
<b>{{unbound topic_count}}</b></a>
</li>
{{/each}}
{{/if}}
</ul>
{{/if}}
</ul>
</section>
</div>

View File

@ -62,7 +62,7 @@
<a href='#' {{action createTopic}}>{{i18n topic.suggest_create_topic}}</a>
{{/if}}
{{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}}
</h3>

View File

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

View File

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

View File

@ -35,7 +35,7 @@
<dt>{{i18n user.last_seen}}:</dt><dd>{{date last_seen_at}}</dd>
{{/if}}
{{#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 email}}
<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() {
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() {

View File

@ -37,11 +37,13 @@ Discourse.ActivityFilterView = Discourse.View.extend({
}
var icon = this.get('icon');
buffer.push("<a href='#'>");
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>");

View File

@ -29,13 +29,14 @@ var Handlebars = {};
;
// lib/handlebars/base.js
Handlebars.VERSION = "1.0.0-rc.4";
Handlebars.COMPILER_REVISION = 3;
Handlebars.VERSION = "1.0.0";
Handlebars.COMPILER_REVISION = 4;
Handlebars.REVISION_CHANGES = {
1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
2: '== 1.0.0-rc.3',
3: '>= 1.0.0-rc.4'
3: '== 1.0.0-rc.4',
4: '>= 1.0.0'
};
Handlebars.helpers = {};
@ -67,7 +68,7 @@ Handlebars.registerHelper('helperMissing', function(arg) {
if(arguments.length === 2) {
return undefined;
} 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 i = 0, ret = "", data;
var type = toString.call(context);
if(type === functionType) { context = context.call(this); }
if (options.data) {
data = Handlebars.createFrame(options.data);
}
@ -152,22 +156,25 @@ Handlebars.registerHelper('each', function(context, options) {
return ret;
});
Handlebars.registerHelper('if', function(context, options) {
var type = toString.call(context);
if(type === functionType) { context = context.call(this); }
Handlebars.registerHelper('if', function(conditional, options) {
var type = toString.call(conditional);
if(type === functionType) { conditional = conditional.call(this); }
if(!context || Handlebars.Utils.isEmpty(context)) {
if(!conditional || Handlebars.Utils.isEmpty(conditional)) {
return options.inverse(this);
} else {
return options.fn(this);
}
});
Handlebars.registerHelper('unless', function(context, options) {
return Handlebars.helpers['if'].call(this, context, {fn: options.inverse, inverse: options.fn});
Handlebars.registerHelper('unless', function(conditional, options) {
return Handlebars.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn});
});
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);
});
@ -181,9 +188,9 @@ Handlebars.registerHelper('log', function(context, options) {
var handlebars = (function(){
var parser = {trace: function trace() { },
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},
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"},
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]],
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:"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],[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,$$,_$) {
var $0 = $$.length - 1;
@ -224,7 +231,10 @@ case 17: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]);
break;
case 18: this.$ = $$[$0-1];
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;
case 20: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], true);
break;
@ -242,7 +252,7 @@ case 26: this.$ = [[$$[$0-1]], $$[$0]];
break;
case 27: this.$ = [[$$[$0]], null];
break;
case 28: this.$ = [[new yy.DataNode($$[$0])], null];
case 28: this.$ = [[$$[$0]], null];
break;
case 29: $$[$0-1].push($$[$0]); this.$ = $$[$0-1];
break;
@ -256,7 +266,7 @@ case 33: this.$ = new yy.IntegerNode($$[$0]);
break;
case 34: this.$ = new yy.BooleanNode($$[$0]);
break;
case 35: this.$ = new yy.DataNode($$[$0]);
case 35: this.$ = $$[$0];
break;
case 36: this.$ = new yy.HashNode($$[$0]);
break;
@ -272,20 +282,26 @@ case 41: this.$ = [$$[$0-2], new yy.IntegerNode($$[$0])];
break;
case 42: this.$ = [$$[$0-2], new yy.BooleanNode($$[$0])];
break;
case 43: this.$ = [$$[$0-2], new yy.DataNode($$[$0])];
case 43: this.$ = [$$[$0-2], $$[$0]];
break;
case 44: this.$ = new yy.PartialNameNode($$[$0]);
break;
case 45: this.$ = new yy.IdNode($$[$0]);
case 45: this.$ = new yy.PartialNameNode(new yy.StringNode($$[$0]));
break;
case 46: $$[$0-2].push($$[$0]); this.$ = $$[$0-2];
case 46: this.$ = new yy.PartialNameNode(new yy.IntegerNode($$[$0]));
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;
}
},
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]}],
defaultActions: {17:[2,1],25:[2,28],38:[2,26],57:[2,24]},
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]},
parseError: function parseError(str, hash) {
throw new Error(str);
},
@ -584,7 +600,7 @@ case 3:
break;
case 4: yy_.yytext = yy_.yytext.substr(0, yy_.yyleng-4); this.popState(); return 15;
break;
case 5: this.begin("par"); return 24;
case 5: return 25;
break;
case 6: return 16;
break;
@ -596,7 +612,7 @@ case 9: return 19;
break;
case 10: return 23;
break;
case 11: return 23;
case 11: return 22;
break;
case 12: this.popState(); this.begin('com');
break;
@ -604,48 +620,44 @@ case 13: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return
break;
case 14: return 22;
break;
case 15: return 36;
case 15: return 37;
break;
case 16: return 35;
case 16: return 36;
break;
case 17: return 35;
case 17: return 36;
break;
case 18: return 39;
case 18: return 40;
break;
case 19: /*ignore whitespace*/
break;
case 20: this.popState(); return 18;
case 20: this.popState(); return 24;
break;
case 21: this.popState(); return 18;
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;
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;
case 24: yy_.yytext = yy_.yytext.substr(1); return 28;
case 24: return 38;
break;
case 25: return 32;
case 25: return 33;
break;
case 26: return 32;
case 26: return 33;
break;
case 27: return 31;
case 27: return 32;
break;
case 28: return 35;
case 28: return 36;
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;
case 30: return 'INVALID';
break;
case 31: /*ignore whitespace*/
break;
case 32: this.popState(); return 37;
break;
case 33: return 5;
case 31: return 5;
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.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.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,31],"inclusive":false},"emu":{"rules":[3],"inclusive":false},"com":{"rules":[4],"inclusive":false},"INITIAL":{"rules":[0,1,2,31],"inclusive":true}};
return lexer;})()
parser.lexer = lexer;
function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser;
@ -731,21 +743,24 @@ Handlebars.AST.HashNode = function(pairs) {
Handlebars.AST.IdNode = function(parts) {
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++) {
var part = parts[i];
var part = parts[i].part;
original += (parts[i].separator || '') + part;
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 { this.isScoped = true; }
}
else { dig.push(part); }
}
this.original = original;
this.parts = dig;
this.string = dig.join('.');
this.depth = depth;
@ -759,7 +774,7 @@ Handlebars.AST.IdNode = function(parts) {
Handlebars.AST.PartialNameNode = function(name) {
this.type = "PARTIAL_NAME";
this.name = name;
this.name = name.original;
};
Handlebars.AST.DataNode = function(id) {
@ -769,13 +784,15 @@ Handlebars.AST.DataNode = function(id) {
Handlebars.AST.StringNode = function(string) {
this.type = "STRING";
this.string = string;
this.stringModeValue = string;
this.original =
this.string =
this.stringModeValue = string;
};
Handlebars.AST.IntegerNode = function(integer) {
this.type = "INTEGER";
this.integer = integer;
this.original =
this.integer = integer;
this.stringModeValue = Number(integer);
};
@ -1162,7 +1179,15 @@ Compiler.prototype = {
DATA: function(data) {
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) {
@ -1361,8 +1386,9 @@ JavaScriptCompiler.prototype = {
if (!this.isChild) {
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 || {};"; }
out.push(copies);
} else {
@ -1391,7 +1417,9 @@ JavaScriptCompiler.prototype = {
// Generate minimizer alias mappings
if (!this.isChild) {
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
lookupData: function(id) {
this.push(this.nameLookup('data', id, 'data'));
this.push('data');
},
// [pushStringParam]
@ -1717,8 +1745,9 @@ JavaScriptCompiler.prototype = {
this.context.aliases.helperMissing = 'helpers.helperMissing';
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) {
return name + ' ? ' + name + '.call(' +
helper.callParams + ") " + ": helperMissing.call(" +
@ -2163,6 +2192,16 @@ Handlebars.VM = {
}
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,
noop: Handlebars.VM.noop,
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) {
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) {
var result = finder();
if (preloadStore.data[key]) {
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 (result.then) {
result.then(function(result) {
return promise.resolve(result);
}, function(result) {
return promise.reject(result);
});
} else {
promise.resolve(result);
}
if (finder) {
return Ember.Deferred.promise(function(promise) {
var result = finder();
// If the finder returns a promise, we support that too
if (result.then) {
result.then(function(result) {
return promise.resolve(result);
}, function(result) {
return promise.reject(result);
});
} else {
promise.resolve(null);
promise.resolve(result);
}
}
});
});
}
return Ember.RSVP.resolve(null);
},
/**

View File

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

View File

@ -55,6 +55,7 @@
// --------------------------------------------------
.nav-stacked {
position: relative;
@extend %nav;
border: 1px solid $nav-stacked-border-color;
padding: 0;
@ -69,7 +70,7 @@
}
> a {
margin: 0;
padding: 13px;
padding: 13px 13px 13px 30px;
font-weight: bold;
font-size: 16px;
line-height: 20px;
@ -94,17 +95,11 @@
line-height: 20px;
}
.no-glyph {
a {
padding-left: 30px;
}
}
.glyph {
font-size: 12px;
margin: 15px 0 0 0;
width: 30px;
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
end
store_preloaded("site", Site.cached_json(current_user))
store_preloaded("site", Site.cached_json(guardian))
if current_user.present?
store_preloaded("currentUser", MultiJson.dump(CurrentUserSerializer.new(current_user, root: false)))

View File

@ -52,16 +52,18 @@ class CategoriesController < ApplicationController
[:name, :color, :text_color]
end
def category_param_keys
[required_param_keys, :hotness, :secure, :group_names, :auto_close_days].flatten!
end
def category_params
required_param_keys.each do |key|
params.require(key)
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
def fetch_category

View File

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

View File

@ -60,22 +60,7 @@ class Users::OmniauthCallbacksController < ApplicationController
auth_provider: "Twitter"
}
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
process_user_info(user_info, screen_name)
end
def create_or_sign_on_user_using_facebook(auth_token)
@ -265,24 +250,7 @@ class Users::OmniauthCallbacksController < ApplicationController
auth_provider: "Github"
}
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
process_user_info(user_info, screen_name)
end
def create_or_sign_on_user_using_persona(auth_token)
@ -319,6 +287,26 @@ class Users::OmniauthCallbacksController < ApplicationController
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?
SiteSetting.invite_only? && !@data[:authenticated]
end

View File

@ -22,6 +22,7 @@ class Category < ActiveRecord::Base
before_validation :ensure_slug
after_save :invalidate_site_cache
before_save :apply_permissions
after_create :create_category_definition
after_create :publish_categories_list
after_destroy :invalidate_site_cache
@ -34,15 +35,52 @@ class Category < ActiveRecord::Base
scope :secured, ->(guardian = nil) {
ids = guardian.secure_category_ids if guardian
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
where("NOT categories.secure")
where("NOT categories.read_restricted")
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'
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
# all categories.
@ -119,28 +157,69 @@ class Category < ActiveRecord::Base
end
end
def deny(group)
if group == :all
self.secure = true
end
# will reset permission on a topic to a particular
# set.
#
# 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
def allow(group)
if group == :all
self.secure = false
# this is kind of annoying, there should be a clean way of queuing this stuff
category_groups.destroy_all unless new_record?
else
groups.push(group)
def permissions=(permissions)
set_permissions(permissions)
end
def apply_permissions
if @permissions
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
def secure_group_ids
if self.secure
if self.read_restricted?
groups.pluck("groups.id")
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
# == Schema Information
@ -162,7 +241,7 @@ end
# description :text
# text_color :string(6) default("FFFFFF"), 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
#
# Indexes

View File

@ -1,16 +1,22 @@
class CategoryGroup < ActiveRecord::Base
belongs_to :category
belongs_to :group
def self.permission_types
@permission_types ||= Enum.new(:full, :create_post, :readonly)
end
end
# == Schema Information
#
# Table name: category_groups
#
# id :integer not null, primary key
# category_id :integer not null
# group_id :integer not null
# created_at :datetime not null
# updated_at :datetime not null
# id :integer not null, primary key
# category_id :integer not null
# group_id :integer not null
# created_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
AUTO_GROUPS = {
:everyone => 0,
:admins => 1,
:moderators => 2,
:staff => 3,
@ -34,6 +35,10 @@ class Group < ActiveRecord::Base
group.save!
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}")
# don't allow shoddy localization to break this

View File

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

View File

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

View File

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

View File

@ -25,11 +25,24 @@ class Site
TrustLevel.all
end
def group_names
@group_name ||= Group.pluck(:name)
end
def categories
Category
.secured(@guardian)
.latest
.includes(:topic_only_relative_url)
@categories ||= begin
categories = Category
.secured(@guardian)
.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
def archetypes

View File

@ -103,9 +103,9 @@ class Topic < ActiveRecord::Base
# Query conditions
condition =
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
["NOT c.secure"]
["NOT c.read_restricted"]
end
where("category_id IS NULL OR category_id IN (
@ -629,8 +629,8 @@ class Topic < ActiveRecord::Base
self
end
def secure_category?
category && category.secure
def read_restricted_category?
category && category.read_restricted
end
private
@ -692,6 +692,7 @@ end
# auto_close_at :datetime
# auto_close_user_id :integer
# auto_close_started_at :datetime
# deleted_by_id :integer
#
# Indexes
#

View File

@ -113,11 +113,11 @@ class TopicTrackingState
((#{unread}) OR (#{new})) AND
(topics.visible OR u.admin OR u.moderator) 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
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
WHERE c2.secure )
WHERE c2.read_restricted )
)
SQL

View File

@ -486,10 +486,14 @@ class User < ActiveRecord::Base
end
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
end
def topic_create_allowed_category_ids
Category.topic_create_allowed(self.id).select(:id)
end
# Flag all posts from a user as spam
def flag_linked_posts_as_spam
admin = Discourse.system_user
@ -660,6 +664,7 @@ end
# topic_reply_count :integer default(0), not null
# blocked :boolean default(FALSE)
# dynamic_favicon :boolean default(FALSE), not null
# title :string(255)
#
# Indexes
#

View File

@ -179,7 +179,7 @@ ORDER BY p.created_at desc
# move into Topic perhaps
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")
end
@ -232,11 +232,11 @@ ORDER BY p.created_at desc
unless guardian.is_staff?
allowed = guardian.secure_category_ids
if allowed.present?
builder.where("( c.secure IS NULL OR
c.secure = 'f' OR
(c.secure = 't' and c.id in (:cats)) )", cats: guardian.secure_category_ids )
builder.where("( c.read_restricted IS NULL OR
NOT c.read_restricted OR
(c.read_restricted and c.id in (:cats)) )", cats: guardian.secure_category_ids )
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

View File

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

View File

@ -1,13 +1,24 @@
class CategorySerializer < BasicCategorySerializer
attributes :secure, :groups, :available_groups, :auto_close_days
attributes :read_restricted, :available_groups, :auto_close_days, :group_permissions
def groups
@groups ||= object.groups.order("name").all.map(&:name)
def group_permissions
@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
def available_groups
Group.order("name").map(&:name) - groups
Group.order(:name).pluck(:name) - group_permissions.map{|g| g[:group_name]}
end
end

View File

@ -3,7 +3,8 @@ class SiteSerializer < ApplicationSerializer
attributes :default_archetype,
:notification_types,
:post_types,
:uncategorized_slug
:uncategorized_slug,
:group_names
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
config.ember.variant = :development
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
# attr_accessible.

View File

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

View File

@ -12,7 +12,10 @@ cs:
storage_units:
format: ! '%n %u'
units:
byte: B
byte:
one: bajt
few: bajty
other: bajtů
gb: GB
kb: KB
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."
actions:
recover: "Vrátit téma"
delete: "Odstranit téma"
open: "Otevřít téma"
close: "Zavřít téma"
@ -775,6 +779,7 @@ cs:
continue_discussion: "Pokračující diskuze z {{postLink}}:"
follow_quote: "přejít na citovaný příspěvek"
deleted_by_author: "(příspěvek odstraněn autorem)"
deleted_by: "odstranil"
expand_collapse: "rozbalit/sbalit"
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."
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_allowed_for_new_user: "Bohužel, noví uživatelé nemohou nahrávat obrázky."
abandon: "Opravdu chcete opustit váš příspěvek?"
@ -998,6 +1004,7 @@ cs:
views_long: "toto téma bylo zobrazeno {{number}}krát"
activity: "Aktivita"
likes: "Líbí se"
likes_long: "je zde {{number}} 'líbí se' v tomto tématu"
top_contributors: "Účastníci"
category_title: "Kategorie"
history: "Historie"
@ -1064,6 +1071,8 @@ cs:
critical_available: "Je k dispozici důležitá aktualizace."
updates_available: "Jsou k dispozici aktualizace."
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"
latest_version: "Poslední verze"
problems_found: "Byly nalezeny problémy s vaší instalací systému Discourse:"
@ -1073,6 +1082,7 @@ cs:
moderators: 'Moderátoři:'
admins: 'Administrátoři:'
blocked: 'Blokováno:'
banned: 'Zakázáno:'
private_messages_short: "SZ"
private_messages_title: "Soukromé zprávy"
@ -1176,6 +1186,7 @@ cs:
settings: "Nastavení"
logs: "Záznamy"
sent_at: "Odesláno"
user: "Uživatel"
email_type: "Typ emailu"
to_address: "Komu"
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."
active: "Aktivní"
nav:
new: "Noví"
active: "Aktivní"
new: "Nový"
pending: "Čeká na schválení"
admins: "Administrátoři"
moderators: "Moderátoři"
banned: "Zakázaní"
blocked: "Blokovaní"
approved: "Schválen?"
approved_selected:
one: "schválit uživatele"
@ -1225,6 +1240,7 @@ cs:
admins: 'Admininstrátoři'
moderators: 'Moderátoři'
blocked: 'Blokovaní uživatelé'
banned: "Zakázaní uživatelé"
user:
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."
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."
trust_level_change_failed: "Nastal problém při změně důveryhodnosti uživatele."
site_content:
none: "Zvolte typ obsahu a můžete začít editovat."

View File

@ -168,6 +168,7 @@ en:
"13": "Inbox"
user:
said: "{{username}} said:"
profile: Profile
mute: Mute
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>.'
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
admin_js:
type_to_filter: "type to filter..."
@ -1245,3 +1251,4 @@ en:
title: 'Settings'
reset: 'reset to default'
none: 'none'

View File

@ -60,6 +60,9 @@ cs:
rss_topics_in_category: "RSS feed témat z kategorie '%{category}'"
author_wrote: "%{author} napsal:"
private_message_abbrev: "SZ"
rss_description:
latest: "Poslední témata"
hot: "Populární témata"
groups:
errors:
@ -139,6 +142,8 @@ cs:
title: "vůdce"
elder:
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:
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."
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)"
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'"
@ -527,6 +533,8 @@ cs:
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_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"
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+"
@ -536,6 +544,8 @@ cs:
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?"
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)"
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."
@ -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_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_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_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:

View File

@ -436,6 +436,9 @@ en:
tos_miscellaneous:
title: "Terms of Service: Miscellaneous"
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:
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_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"
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"
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"

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;">
<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>
<td style="padding: 10px; background-color: #f9f9f9">{{{quote}}}</td>

View File

@ -7,6 +7,7 @@ class Guardian
def staff?; false; end
def approved?; false; end
def secure_category_ids; []; end
def topic_create_allowed_category_ids; []; end
def has_trust_level?(level); false; end
end
@ -237,8 +238,19 @@ class Guardian
can_create_post?(parent)
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)
!SpamRulesEnforcer.block?(@user)
!SpamRulesEnforcer.block?(@user) && (
!parent ||
!parent.category ||
Category.post_create_allowed(self).where(:id => parent.category.id).count == 1
)
end
def can_create_post_on_topic?(topic)
@ -328,7 +340,7 @@ class Guardian
topic.deleted_at.nil? &&
# 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(topic.private_message?) || authenticated? && (topic.all_allowed_users.where(id: @user.id).exists? || is_admin?))
@ -340,7 +352,7 @@ class Guardian
end
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
def can_vote?(post, opts={})
@ -378,6 +390,10 @@ class Guardian
@secure_category_ids ||= @user.secure_category_ids
end
def topic_create_allowed_category_ids
@topic_create_allowed_category_ids ||= @user.topic_create_allowed_category_ids
end
private
def is_my_own?(obj)

View File

@ -19,9 +19,9 @@ module Oneboxer
case route[:controller]
when 'users'
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[: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
return nil unless post
Guardian.new.ensure_can_see!(post)
return @url unless Guardian.new.can_see?(post)
topic = post.topic
slug = Slug.for(topic.title)
@ -52,7 +52,8 @@ module Oneboxer
topic = Topic.where(id: route[:topic_id].to_i).includes(:user).first
return nil unless topic
Guardian.new.ensure_can_see!(topic)
return @url unless Guardian.new.can_see?(topic)
post = topic.posts.first
posters = topic.posters_summary.map do |p|

View File

@ -116,7 +116,7 @@ class PostCreator
protected
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
end
end

View File

@ -46,6 +46,19 @@ module PrettyText
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
def avatar_template(username)
return "" unless username
@ -90,6 +103,7 @@ module PrettyText
@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 I18n = {}; I18n.t = function(a,b){ return helpers.t(a,b); }");
ctx_load( "app/assets/javascripts/discourse/components/bbcode.js",
"app/assets/javascripts/discourse/components/utilities.js",

View File

@ -160,9 +160,9 @@ class Search
.order("topics.bumped_at DESC")
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
posts = posts.where("(categories.id IS NULL) OR (NOT categories.secure)")
posts = posts.where("(categories.id IS NULL) OR (NOT categories.read_restricted)")
end
posts.limit(limit)
end

View File

@ -18,9 +18,9 @@ class SqlBuilder
def secure_category(secure_category_ids, category_alias = 'c')
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
where("NOT COALESCE(" << category_alias << ".secure, false)")
where("NOT COALESCE(" << category_alias << ".read_restricted, false)")
end
self
end

View File

@ -27,9 +27,9 @@ class TopicCreator
topic_params[:archetype] = @opts[:archetype] if @opts[:archetype].present?
topic_params[:subtype] = @opts[:subtype] if @opts[:subtype].present?
@guardian.ensure_can_create!(Topic)
category = Category.where(name: @opts[:category]).first
@guardian.ensure_can_create!(Topic,category)
topic_params[:category_id] = category.id if category.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?

View File

@ -239,9 +239,9 @@ class TopicQuery
unless @user && @user.moderator?
category_ids = @user.secure_category_ids if @user
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
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

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)
topic = Fabricate(:topic, category: cat)
cat.deny(:all)
cat.allow(Group[:admins])
cat.set_permissions(:admins => :full)
cat.save
CategoryList.new(Guardian.new admin).categories.count.should == 1

View File

@ -215,8 +215,9 @@ describe Guardian do
it 'correctly handles groups' do
group = Fabricate(:group)
category = Fabricate(:category, secure: true)
category.allow(group)
category = Fabricate(:category, read_restricted: true)
category.set_permissions(group => :full)
category.save
topic = Fabricate(:topic, category: category)
@ -275,8 +276,27 @@ describe Guardian do
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
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
Guardian.new.can_create?(Post, topic).should be_false
end

View File

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

View File

@ -15,15 +15,15 @@ test
end
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
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
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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
require 'spec_helper'
require "spec_helper"
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)
end
describe 'logged in' do
describe "logged in" do
before do
@user = log_in(:moderator)
end
@ -18,55 +18,66 @@ describe CategoriesController do
response.should be_forbidden
end
it 'raises an exception when the name is missing' do
lambda { xhr :post, :create, color: 'ff0', text_color: 'fff' }.should raise_error(ActionController::ParameterMissing)
it "raises an exception when the name is missing" do
lambda { xhr :post, :create, color: "ff0", text_color: "fff" }.should raise_error(ActionController::ParameterMissing)
end
it 'raises an exception when the color is missing' do
lambda { xhr :post, :create, name: 'hello', text_color: 'fff' }.should raise_error(ActionController::ParameterMissing)
it "raises an exception when the color is missing" do
lambda { xhr :post, :create, name: "hello", text_color: "fff" }.should raise_error(ActionController::ParameterMissing)
end
it 'raises an exception when the text color is missing' do
lambda { xhr :post, :create, name: 'hello', color: 'ff0' }.should raise_error(ActionController::ParameterMissing)
it "raises an exception when the text color is missing" do
lambda { xhr :post, :create, name: "hello", color: "ff0" }.should raise_error(ActionController::ParameterMissing)
end
describe 'failure' do
describe "failure" do
before do
@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
it { should_not respond_with(:success) }
it 'returns errors on a duplicate category name' do
response.code.to_i.should == 422
it "returns errors on a duplicate category name" do
response.status.should == 422
end
end
describe 'success' do
before do
xhr :post, :create, name: 'hello', color: 'ff0', text_color: 'fff'
describe "success" do
it "works" do
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
it 'creates a category' do
Category.count.should == 1
end
it { should respond_with(:success) }
end
end
end
describe 'destroy' do
describe "destroy" do
it 'requires the user to be logged in' do
lambda { xhr :delete, :destroy, id: 'category'}.should raise_error(Discourse::NotLoggedIn)
it "requires the user to be logged in" do
lambda { xhr :delete, :destroy, id: "category"}.should raise_error(Discourse::NotLoggedIn)
end
describe 'logged in' do
describe "logged in" do
before do
@user = log_in
@category = Fabricate(:category, user: @user)
@ -86,14 +97,14 @@ describe CategoriesController do
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)
end
describe 'logged in' do
describe "logged in" do
before do
@user = log_in(:moderator)
@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)
end
describe 'failure' do
describe "failure" do
before do
@other_category = Fabricate(:category, name: 'Other', user: @user )
xhr :put, :update, id: @category.id, name: @other_category.name, color: 'ff0', text_color: 'fff'
@other_category = Fabricate(:category, name: "Other", user: @user )
xhr :put, :update, id: @category.id, name: @other_category.name, color: "ff0", text_color: "fff"
end
it 'returns errors on a duplicate category name' do
it "returns errors on a duplicate category name" do
response.should_not be_success
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
end
end
describe 'success' do
before do
# might as well test this as well
@category.allow(Group[:admins])
@category.save
describe "success" do
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
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

View File

@ -15,8 +15,7 @@ describe CategoryFeaturedTopic do
# so much dancing, I am thinking fixures make sense here.
user.change_trust_level!(:basic)
category.deny(:all)
category.allow(Group[:trust_level_1])
category.set_permissions(:trust_level_1 => :full)
category.save
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 :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
let(:category) { Fabricate(:category) }
let(:category_2) { Fabricate(:category) }
@ -25,20 +82,20 @@ describe Category do
let(:group) { Fabricate(:group) }
it "secures categories correctly" do
category.secure?.should be_false
category.read_restricted?.should be_false
category.deny(:all)
category.secure?.should be_true
category.set_permissions({})
category.read_restricted?.should be_true
category.allow(:all)
category.secure?.should be_false
category.set_permissions(:everyone => :full)
category.read_restricted?.should be_false
user.secure_categories.should be_empty
group.add(user)
group.save
category.allow(group)
category.set_permissions(group.id => :full)
category.save
user.reload
@ -47,13 +104,13 @@ describe Category do
it "lists all secured categories correctly" do
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_2.allow(group)
Category.secured.should =~ [category, category_2]
Category.secured.should =~ []
Category.secured(Guardian.new(user)).should =~ [category, category_2]
end
end

View File

@ -80,6 +80,11 @@ describe PostAnalyzer do
post_analyzer = PostAnalyzer.new(raw_three_links, default_topic_id)
post_analyzer.linked_hosts.should == {"discourse.org" => 1, "www.imdb.com" => 1}
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

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.counts_for(Guardian.new, post.topic, [post]).length.should == 1
category.deny(:all)
category.allow(Group[:staff])
category.set_permissions(:staff => :full)
category.save
admin = Fabricate(:admin)

View File

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

View File

@ -51,7 +51,7 @@ describe TopicTrackingState do
row.user_id.should == post.user_id
# 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.save

View File

@ -68,7 +68,7 @@ describe UserAction do
# groups
category = Fabricate(:category, secure: true)
category = Fabricate(:category, read_restricted: true)
public_topic.recover!
public_topic.category = category
@ -82,7 +82,7 @@ describe UserAction do
group.add(u)
group.save
category.allow(group)
category.set_permissions(group => :full)
category.save
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.
format("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]",
"</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");
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 " +
"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-" +
"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");
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" +
"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 " +
"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");
});

View File

@ -69,13 +69,13 @@ test("Quotes", function() {
cookedOptions("1[quote=\"bob, post:1\"]my quote[/quote]2",
{ 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" +
" 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");
cookedOptions("1[quote=\"bob, post:1\"]my quote[/quote]2",
{ 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" +
" \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");
});

View File

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

View File

@ -14,7 +14,7 @@ test("remove", function() {
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);
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);
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);
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);
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);
PreloadStore.getAndRemove('bane').then(function(result) {

View File

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

View File

@ -18,4 +18,15 @@ function controllerFor(controller, model) {
var controller = Discourse.__container__.lookup('controller:' + controller);
if (model) { controller.set('model', model ); }
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;
}
return asyncTest(name, function() {
return asyncTestDiscourse(name, function() {
qHint.sendRequest(sourceFile, function(req) {
start();
@ -113,9 +113,8 @@ var jsHintOpts = {
"visit",
"count",
"exists",
"asyncTest",
"asyncTestDiscourse",
"find",
"resolvingPromise",
"sinon",
"moment",
"start",
@ -125,7 +124,6 @@ var jsHintOpts = {
"controllerFor",
"containsInstance",
"deepEqual",
"resolvingPromiseWith",
"Blob",
"File"],
"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);
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);
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");
});
asyncTest("loadIntoIdentityMap with no data", function() {
asyncTestDiscourse("loadIntoIdentityMap with no data", function() {
var postStream = buildStream(1234);
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);
expect(1);
this.stub(Discourse, "ajax").returns(resolvingPromiseWith({
this.stub(Discourse, "ajax").returns(Ember.RSVP.resolve({
post_stream: {
posts: [{id: 10, post_number: 10}]
}

View File

@ -11,7 +11,7 @@
// Externals we need to load first
//= require ../../app/assets/javascripts/external/jquery-1.9.1.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/group-helper.js
@ -46,11 +46,11 @@
// sinon settings
sinon.config = {
injectIntoThis: true,
injectInto: null,
properties: ["spy", "stub", "mock", "clock", "sandbox"],
useFakeTimers: false,
useFakeServer: false
injectIntoThis: true,
injectInto: null,
properties: ["spy", "stub", "mock", "clock", "sandbox"],
useFakeTimers: false,
useFakeServer: false
};
window.assetPath = function() { return null };