catching up with the master

Merge remote-tracking branch 'upstream/master'
This commit is contained in:
Kris Aubuchon 2013-10-28 12:26:12 -04:00
commit cafc1a088d
130 changed files with 2564 additions and 877 deletions

View File

@ -0,0 +1,64 @@
/**
A form to create an IP address that will be blocked or whitelisted.
Example usage:
{{screened-ip-address-form action="recordAdded"}}
where action is a callback on the controller or route that will get called after
the new record is successfully saved. It is called with the new ScreenedIpAddress record
as an argument.
@class ScreenedIpAddressFormComponent
@extends Ember.Component
@namespace Discourse
@module Discourse
**/
Discourse.ScreenedIpAddressFormComponent = Ember.Component.extend({
classNames: ['screened-ip-address-form'],
formSubmitted: false,
actionName: 'block',
actionNames: function() {
return [
{id: 'block', name: I18n.t('admin.logs.screened_ips.actions.block')},
{id: 'do_nothing', name: I18n.t('admin.logs.screened_ips.actions.do_nothing')}
];
}.property(),
actions: {
submit: function() {
if (!this.get('formSubmitted')) {
var self = this;
this.set('formSubmitted', true);
var screenedIpAddress = Discourse.ScreenedIpAddress.create({ip_address: this.get('ip_address'), action_name: this.get('actionName')});
screenedIpAddress.save().then(function(result) {
self.set('ip_address', '');
self.set('formSubmitted', false);
self.sendAction('action', Discourse.ScreenedIpAddress.create(result.screened_ip_address));
Em.run.schedule('afterRender', function() { self.$('.ip-address-input').focus(); });
}, function(e) {
self.set('formSubmitted', false);
var msg;
if (e.responseJSON && e.responseJSON.errors) {
msg = I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')});
} else {
msg = I18n.t("generic_error");
}
bootbox.alert(msg, function() { self.$('.ip-address-input').focus(); });
});
}
}
},
didInsertElement: function(e) {
var self = this;
this._super();
Em.run.schedule('afterRender', function() {
self.$('.ip-address-input').keydown(function(e) {
if (e.keyCode === 13) { // enter key
self.send('submit');
}
});
});
}
});

View File

@ -18,6 +18,12 @@ Discourse.AdminLogsScreenedIpAddressesController = Ember.ArrayController.extend(
self.set('content', result); self.set('content', result);
self.set('loading', false); self.set('loading', false);
}); });
},
actions: {
recordAdded: function(arg) {
this.get("content").unshiftObject(arg);
}
} }
}); });
@ -27,12 +33,12 @@ Discourse.AdminLogsScreenedIpAddressController = Ember.ObjectController.extend({
actions: { actions: {
allow: function(record) { allow: function(record) {
record.set('action', 'do_nothing'); record.set('action_name', 'do_nothing');
this.send('save', record); this.send('save', record);
}, },
block: function(record) { block: function(record) {
record.set('action', 'block'); record.set('action_name', 'block');
this.send('save', record); this.send('save', record);
}, },

View File

@ -9,20 +9,20 @@
**/ **/
Discourse.ScreenedIpAddress = Discourse.Model.extend({ Discourse.ScreenedIpAddress = Discourse.Model.extend({
actionName: function() { actionName: function() {
return I18n.t("admin.logs.screened_ips.actions." + this.get('action')); return I18n.t("admin.logs.screened_ips.actions." + this.get('action_name'));
}.property('action'), }.property('action_name'),
isBlocked: function() { isBlocked: function() {
return (this.get('action') === 'block'); return (this.get('action_name') === 'block');
}.property('action'), }.property('action_name'),
actionIcon: function() { actionIcon: function() {
if (this.get('action') === 'block') { if (this.get('action_name') === 'block') {
return this.get('blockIcon'); return this.get('blockIcon');
} else { } else {
return this.get('doNothingIcon'); return this.get('doNothingIcon');
} }
}.property('action'), }.property('action_name'),
blockIcon: function() { blockIcon: function() {
return 'icon-ban-circle'; return 'icon-ban-circle';
@ -33,9 +33,9 @@ Discourse.ScreenedIpAddress = Discourse.Model.extend({
}.property(), }.property(),
save: function() { save: function() {
return Discourse.ajax("/admin/logs/screened_ip_addresses/" + this.get('id') + ".json", { return Discourse.ajax("/admin/logs/screened_ip_addresses" + (this.id ? '/' + this.id : '') + ".json", {
type: 'PUT', type: this.id ? 'PUT' : 'POST',
data: {ip_address: this.get('ip_address'), action_name: this.get('action')} data: {ip_address: this.get('ip_address'), action_name: this.get('action_name')}
}); });
}, },

View File

@ -1,5 +1,8 @@
<p>{{i18n admin.logs.screened_ips.description}}</p> <p>{{i18n admin.logs.screened_ips.description}}</p>
{{screened-ip-address-form action="recordAdded"}}
<br/>
{{#if loading}} {{#if loading}}
<div class='admin-loading'>{{i18n loading}}</div> <div class='admin-loading'>{{i18n loading}}</div>
{{else}} {{else}}

View File

@ -7,7 +7,7 @@
//= require ./env //= require ./env
// probe framework first // probe framework first
//= require ./discourse/components/probes.js //= require ./discourse/lib/probes.js
// Externals we need to load first // Externals we need to load first

View File

@ -1,5 +1,22 @@
Discourse.DiscourseBreadcrumbsComponent = Ember.Component.extend({ Discourse.DiscourseBreadcrumbsComponent = Ember.Component.extend({
classNames: ['category-breadcrumb'], classNames: ['category-breadcrumb'],
tagName: 'ol', tagName: 'ol',
parentCategory: Em.computed.alias('category.parentCategory') parentCategory: Em.computed.alias('category.parentCategory'),
parentCategories: Em.computed.filter('categories', function(c) {
return !c.get('parentCategory');
}),
targetCategory: function() {
// Note we can't use Em.computed.or here because it returns a boolean not the object
return this.get('parentCategory') || this.get('category');
}.property('parentCategory', 'category'),
childCategories: function() {
var self = this;
return this.get('categories').filter(function (c) {
return c.get('parentCategory') === self.get('targetCategory');
});
}.property('targetCategory')
}); });

View File

@ -0,0 +1,54 @@
Discourse.DiscourseCategorydropComponent = Ember.Component.extend({
classNameBindings: ['category::no-category', 'categories:has-drop'],
tagName: 'li',
iconClass: function() {
if (this.get('expanded')) { return "icon icon-caret-down"; }
return "icon icon-caret-right";
}.property('expanded'),
badgeStyle: function() {
var category = this.get('category');
if (category) {
return Discourse.HTML.categoryStyle(category);
} else {
return "background-color: #eee; color: #333";
}
}.property('category'),
actions: {
expand: function() {
if (this.get('expanded')) {
this.close();
return;
}
if (this.get('categories')) {
this.set('expanded', true);
}
var self = this,
$dropdown = this.$()[0];
$('html').on('click.category-drop', function(e) {
var $target = $(e.target),
closest = $target.closest($dropdown);
return ($(e.currentTarget).hasClass('badge-category') || (closest.length && closest[0] === $dropdown)) ? true : self.close();
});
}
},
categoryChanged: function() {
this.close();
}.observes('category', 'parentCategory'),
close: function() {
$('html').off('click.category-drop');
this.set('expanded', false);
},
willDestroyElement: function() {
$('html').off('click.category-drop');
}
});

View File

@ -13,6 +13,12 @@ Discourse.EditCategoryController = Discourse.ObjectController.extend(Discourse.M
settingsSelected: Ember.computed.equal('selectedTab', 'settings'), settingsSelected: Ember.computed.equal('selectedTab', 'settings'),
foregroundColors: ['FFFFFF', '000000'], foregroundColors: ['FFFFFF', '000000'],
parentCategories: function() {
return Discourse.Category.list().filter(function (c) {
return !c.get('parentCategory');
});
}.property(),
onShow: function() { onShow: function() {
this.changeSize(); this.changeSize();
this.titleChanged(); this.titleChanged();
@ -122,17 +128,27 @@ Discourse.EditCategoryController = Discourse.ObjectController.extend(Discourse.M
}, },
saveCategory: function() { saveCategory: function() {
var categoryController = this; var self = this,
model = this.get('model'),
parentCategory = Discourse.Category.list().findBy('id', parseInt(model.get('parent_category_id'), 10));
this.set('saving', true); this.set('saving', true);
model.set('parentCategory', parentCategory);
var newSlug = Discourse.Category.slugFor(this.get('model'));
this.get('model').save().then(function(result) { this.get('model').save().then(function(result) {
// success // success
categoryController.send('closeModal'); self.send('closeModal');
Discourse.URL.redirectTo("/category/" + Discourse.Category.slugFor(result.category)); Discourse.URL.redirectTo("/category/" + newSlug);
}, function(errors) { }, function(error) {
// errors
if(errors.length === 0) errors.push(I18n.t("category.creation_error")); if (error && error.responseText) {
categoryController.displayErrors(errors); self.flash($.parseJSON(error.responseText).errors[0]);
categoryController.set('saving', false); } else {
self.flash(I18n.t('generic_error'));
}
self.set('saving', false);
}); });
}, },
@ -147,8 +163,14 @@ Discourse.EditCategoryController = Discourse.ObjectController.extend(Discourse.M
// success // success
self.send('closeModal'); self.send('closeModal');
Discourse.URL.redirectTo("/categories"); Discourse.URL.redirectTo("/categories");
}, function(jqXHR){ }, function(error){
// error
if (error && error.responseText) {
self.flash($.parseJSON(error.responseText).errors[0]);
} else {
self.flash(I18n.t('generic_error'));
}
self.send('showModal'); self.send('showModal');
self.displayErrors([I18n.t("category.delete_error")]); self.displayErrors([I18n.t("category.delete_error")]);
self.set('deleting', false); self.set('deleting', false);

View File

@ -7,32 +7,34 @@
@module Discourse @module Discourse
**/ **/
Discourse.ListController = Discourse.Controller.extend({ Discourse.ListController = Discourse.Controller.extend({
categoryBinding: 'topicList.category', categoryBinding: "topicList.category",
canCreateCategory: false, canCreateCategory: false,
canCreateTopic: false, canCreateTopic: false,
needs: ['composer', 'modal', 'listTopics'], needs: ["composer", "modal", "listTopics"],
availableNavItems: function() { availableNavItems: function() {
var loggedOn = !!Discourse.User.current(); var loggedOn = !!Discourse.User.current();
var category = this.get("category");
return Discourse.SiteSettings.top_menu.split("|").map(function(i) { return Discourse.SiteSettings.top_menu.split("|").map(function(i) {
return Discourse.NavItem.fromText(i, { return Discourse.NavItem.fromText(i, {
loggedOn: loggedOn loggedOn: loggedOn,
category: category
}); });
}).filter(function(i) { }).filter(function(i) {
return i !== null; return i !== null && !(category && i.get("name").indexOf("categor") === 0);
}); });
}.property(), }.property("category"),
createTopicText: function() { createTopicText: function() {
if (this.get('category.name')) { if (this.get("category.name")) {
return I18n.t("topic.create_in", { return I18n.t("topic.create_in", {
categoryName: this.get('category.name') categoryName: this.get("category.name")
}); });
} else { } else {
return I18n.t("topic.create"); return I18n.t("topic.create");
} }
}.property('category.name'), }.property("category.name"),
/** /**
Refresh our current topic list Refresh our current topic list
@ -132,7 +134,11 @@ Discourse.ListController = Discourse.Controller.extend({
} else { } else {
return false; return false;
} }
}.property('category') }.property('category'),
categories: function() {
return Discourse.Category.list();
}.property()
}); });

View File

@ -7,8 +7,15 @@
@module Discourse @module Discourse
**/ **/
Discourse.StaticController = Discourse.Controller.extend({ Discourse.StaticController = Discourse.Controller.extend({
needs: ['header'],
path: null,
showLoginButton: function() {
return this.get('path') === '/login';
}.property('path'),
loadPath: function(path) { loadPath: function(path) {
this.set('path', path);
var staticController = this; var staticController = this;
this.set('content', null); this.set('content', null);

View File

@ -29,12 +29,32 @@ Handlebars.registerHelper('shorten', function(property, options) {
@for Handlebars @for Handlebars
**/ **/
Handlebars.registerHelper('topicLink', function(property, options) { Handlebars.registerHelper('topicLink', function(property, options) {
var title, topic; var topic = Ember.Handlebars.get(this, property, options),
topic = Ember.Handlebars.get(this, property, options); title = topic.get('fancy_title') || topic.get('title');
title = topic.get('fancy_title') || topic.get('title'); return "<a href='" + topic.get('lastUnreadUrl') + "' class='title'>" + title + "</a>";
return "<a href='" + (topic.get('lastUnreadUrl')) + "' class='title'>" + title + "</a>";
}); });
/**
Produces a link to a category given a category object and helper options
@method categoryLinkHTML
@param {Discourse.Category} category to link to
@param {Object} options standard from handlebars
**/
function categoryLinkHTML(category, options) {
var categoryOptions = {};
if (options.hash) {
if (options.hash.allowUncategorized) {
categoryOptions.allowUncategorized = true;
}
if (options.hash.categories) {
categoryOptions.categories = Em.Handlebars.get(this, options.hash.categories, options);
}
}
return new Handlebars.SafeString(Discourse.HTML.categoryLink(category, categoryOptions));
}
/** /**
Produces a link to a category Produces a link to a category
@ -42,21 +62,16 @@ Handlebars.registerHelper('topicLink', function(property, options) {
@for Handlebars @for Handlebars
**/ **/
Handlebars.registerHelper('categoryLink', function(property, options) { Handlebars.registerHelper('categoryLink', function(property, options) {
var allowUncategorized = options.hash && options.hash.allowUncategorized; return categoryLinkHTML(Ember.Handlebars.get(this, property, options), options);
var category = Ember.Handlebars.get(this, property, options);
return new Handlebars.SafeString(Discourse.Utilities.categoryLink(category, allowUncategorized));
}); });
/** /**
Produces a bound link to a category Produces a bound link to a category
@method boundCategoryLink @method boundCategoryLink
@for Handlebars @for Handlebars
**/ **/
Ember.Handlebars.registerBoundHelper('boundCategoryLink', function(category) { Ember.Handlebars.registerBoundHelper('boundCategoryLink', categoryLinkHTML);
return new Handlebars.SafeString(Discourse.Utilities.categoryLink(category));
});
/** /**
Produces a link to a route with support for i18n on the title Produces a link to a route with support for i18n on the title

View File

@ -0,0 +1,60 @@
/**
Helpers to build HTML strings such as rich links to categories and topics.
@class HTML
@namespace Discourse
@module Discourse
**/
Discourse.HTML = {
/**
Returns the CSS styles for a category
@method categoryStyle
@param {Discourse.Category} category the category whose link we want
**/
categoryStyle: function(category) {
var color = Em.get(category, 'color'),
textColor = Em.get(category, 'text_color');
if (!color && !textColor) { return; }
// Add the custom style if we need to
var style = "";
if (color) { style += "background-color: #" + color + "; "; }
if (textColor) { style += "color: #" + textColor + "; "; }
return style;
},
/**
Create a badge-like category link
@method categoryLink
@param {Discourse.Category} category the category whose link we want
@param {Object} opts The options for the category link
@param {Boolean} opts.allowUncategorized Whether we allow rendering of the uncategorized category
@returns {String} the html category badge
**/
categoryLink: function(category, opts) {
opts = opts || {};
if ((!category) ||
(!opts.allowUncategorized && Em.get(category, 'id') === Discourse.Site.currentProp("uncategorized_category_id"))) return "";
var name = Em.get(category, 'name'),
description = Em.get(category, 'description'),
html = "<a href=\"" + Discourse.getURL("/category/") + Discourse.Category.slugFor(category) + "\" class=\"badge-category\" ";
// Add description if we have it
if (description) html += "title=\"" + Handlebars.Utils.escapeExpression(description) + "\" ";
var categoryStyle = Discourse.HTML.categoryStyle(category);
if (categoryStyle) {
html += "style=\"" + categoryStyle + "\" ";
}
html += ">" + name + "</a>";
return html;
}
};

View File

@ -33,29 +33,6 @@ Discourse.Utilities = {
} }
}, },
/**
Create a badge-like category link
@method categoryLink
@param {Discourse.Category} category the category whose link we want
@returns {String} the html category badge
**/
categoryLink: function(category, allowUncategorized) {
if (!category) return "";
if (!allowUncategorized && Em.get(category, 'id') === Discourse.Site.currentProp("uncategorized_category_id")) return "";
var color = Em.get(category, 'color'),
textColor = Em.get(category, 'text_color'),
name = Em.get(category, 'name'),
description = Em.get(category, 'description'),
html = "<a href=\"" + Discourse.getURL("/category/") + Discourse.Category.slugFor(category) + "\" class=\"badge-category\" ";
// Add description if we have it
if (description) html += "title=\"" + Handlebars.Utils.escapeExpression(description) + "\" ";
return html + "style=\"background-color: #" + color + "; color: #" + textColor + ";\">" + name + "</a>";
},
avatarUrl: function(template, size) { avatarUrl: function(template, size) {
if (!template) { return ""; } if (!template) { return ""; }
var rawSize = Discourse.Utilities.getRawSize(Discourse.Utilities.translateSize(size)); var rawSize = Discourse.Utilities.getRawSize(Discourse.Utilities.translateSize(size));

View File

@ -34,6 +34,13 @@ Discourse.Category = Discourse.Model.extend({
return Discourse.getURL("/category/") + (this.get('slug')); return Discourse.getURL("/category/") + (this.get('slug'));
}.property('name'), }.property('name'),
unreadUrl: function() {
return this.get('url') + '/unread';
}.property('url'),
newUrl: function() {
return this.get('url') + '/new';
}.property('url'),
style: function() { style: function() {
return "background-color: #" + (this.get('category.color')) + "; color: #" + (this.get('category.text_color')) + ";"; return "background-color: #" + (this.get('category.color')) + "; color: #" + (this.get('category.text_color')) + ";";
@ -58,7 +65,8 @@ Discourse.Category = Discourse.Model.extend({
secure: this.get('secure'), secure: this.get('secure'),
permissions: this.get('permissionsForUpdate'), permissions: this.get('permissionsForUpdate'),
auto_close_days: this.get('auto_close_days'), auto_close_days: this.get('auto_close_days'),
position: this.get('position') position: this.get('position'),
parent_category_id: this.get('parent_category_id')
}, },
type: this.get('id') ? 'PUT' : 'POST' type: this.get('id') ? 'PUT' : 'POST'
}); });

View File

@ -31,18 +31,32 @@ Discourse.NavItem = Discourse.Model.extend({
// href from this item // href from this item
href: function() { href: function() {
return Discourse.getURL("/") + this.get('filterMode');
}.property('filterMode'),
// href from this item
filterMode: function() {
var name = this.get('name'); var name = this.get('name');
if( name.split('/')[0] === 'category' ) { if( name.split('/')[0] === 'category' ) {
return Discourse.getURL("/") + 'category/' + this.get('categorySlug'); return 'category/' + this.get('categorySlug');
} else { } else {
return Discourse.getURL("/") + name.replace(' ', '-'); var mode = "";
var category = this.get("category");
if(category){
mode += "category/";
var parentSlug = category.get('parentCategory.slug');
if (parentSlug) { mode += parentSlug + "/"; }
mode += category.get("slug") + "/l/";
}
return mode + name.replace(' ', '-');
} }
}.property('name'), }.property('name'),
count: function() { count: function() {
var state = this.get('topicTrackingState'); var state = this.get('topicTrackingState');
if (state) { if (state) {
return state.lookupCount(this.get('name')); return state.lookupCount(this.get('name'), this.get('category'));
} }
}.property('topicTrackingState.messageCount'), }.property('topicTrackingState.messageCount'),
@ -71,7 +85,8 @@ Discourse.NavItem.reopenClass({
opts = { opts = {
name: name, name: name,
hasIcon: name === "unread" || name === "favorited", hasIcon: name === "unread" || name === "favorited",
filters: split.splice(1) filters: split.splice(1),
category: opts.category
}; };
return Discourse.NavItem.create(opts); return Discourse.NavItem.create(opts);

View File

@ -146,7 +146,6 @@ Discourse.TopicList.reopenClass({
return Ember.RSVP.resolve(list); return Ember.RSVP.resolve(list);
} }
session.setProperties({topicList: null, topicListScrollPos: null}); session.setProperties({topicList: null, topicListScrollPos: null});
return Discourse.TopicList.find(filter, menuItem.get('excludeCategory')); return Discourse.TopicList.find(filter, menuItem.get('excludeCategory'));
} }
}); });

View File

@ -159,15 +159,16 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
return count; return count;
}, },
lookupCount: function(name){ lookupCount: function(name, category){
var categoryName = Em.get(category, "name");
if(name==="new") { if(name==="new") {
return this.countNew(); return this.countNew(categoryName);
} else if(name==="unread") { } else if(name==="unread") {
return this.countUnread(); return this.countUnread(categoryName);
} else { } else {
var category = name.split("/")[1]; categoryName = name.split("/")[1];
if(category) { if(categoryName) {
return this.countCategory(category); return this.countCategory(categoryName);
} }
} }
}, },

View File

@ -26,8 +26,10 @@ Discourse.Route.buildRoutes(function() {
Discourse.ListController.filters.forEach(function(filter) { Discourse.ListController.filters.forEach(function(filter) {
router.route(filter, { path: "/" + filter }); router.route(filter, { path: "/" + filter });
router.route(filter, { path: "/" + filter + "/more" }); router.route(filter, { path: "/" + filter + "/more" });
router.route(filter + "Category", { path: "/category/:slug/" + filter }); router.route(filter + "Category", { path: "/category/:slug/l/" + filter });
router.route(filter + "Category", { path: "/category/:slug/" + filter + "/more" }); router.route(filter + "Category", { path: "/category/:slug/l/" + filter + "/more" });
router.route(filter + "Category", { path: "/category/:parentSlug/:slug/l/" + filter });
router.route(filter + "Category", { path: "/category/:parentSlug/:slug/l/" + filter + "/more" });
}); });

View File

@ -22,11 +22,13 @@ Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend({
} }
var listController = this.controllerFor('list'), var listController = this.controllerFor('list'),
urlId = Discourse.Category.slugFor(category), categorySlug = Discourse.Category.slugFor(category),
self = this; self = this,
filter = this.filter || "latest",
url = "category/" + categorySlug + "/l/" + filter;
listController.set('filterMode', "category/" + urlId); listController.set('filterMode', url);
listController.load("category/" + urlId).then(function(topicList) { listController.load(url).then(function(topicList) {
listController.setProperties({ listController.setProperties({
canCreateTopic: topicList.get('can_create_topic'), canCreateTopic: topicList.get('can_create_topic'),
category: category category: category

View File

@ -1,17 +1,12 @@
<li> <li>
<a href="/">{{title}}</a> {{discourse-categorydrop parentCategory=category categories=parentCategories}}
<i class='icon icon-caret-right first-caret'></i> </li>
<li>
{{discourse-categorydrop parentCategory=category category=targetCategory categories=childCategories}}
</li> </li>
{{#if parentCategory}} {{#if parentCategory}}
<li> <li>
{{discourse-categorydrop category=parentCategory categories=categories}} {{boundCategoryLink category}}
</li>
{{/if}}
{{#if category}}
<li>
{{discourse-categorydrop category=category}}
</li> </li>
{{/if}} {{/if}}

View File

@ -1,4 +1,12 @@
{{categoryLink category}} {{#if category}}
{{#if categories}} {{boundCategoryLink category allowUncategorized=true}}
<button {{action expand}}><i class='icon icon-caret-right'></i></button> {{else}}
<a href='/' class='badge-category home' {{bindAttr style="badgeStyle"}}><i class='icon icon-home'></i></a>
{{/if}}
{{#if categories}}
<a href='#' {{action expand}} class='badge-category category-dropdown-button' {{bindAttr style="badgeStyle"}}><i {{bindAttr class="iconClass"}}></i></a>
<section {{bindAttr class="expanded::hidden :category-dropdown-menu"}} class='chooser'>
{{#each categories}}<div class='cat'>{{categoryLink this allowUncategorized=true}}</div>{{/each}}
</section>
{{/if}} {{/if}}

View File

@ -0,0 +1,4 @@
<b>{{i18n admin.logs.screened_ips.form.label}}</b>
{{textField value=ip_address disabled=formSubmitted class="ip-address-input" placeholderKey="admin.logs.screened_ips.form.ip_address" autocorrect="off" autocapitalize="off"}}
{{combobox content=actionNames value=actionName}}
<button class="btn btn-small" {{action submit target="view"}} {{bindAttr disabled="formSubmitted"}}>{{i18n admin.logs.screened_ips.form.add}}</button>

View File

@ -144,7 +144,7 @@
{{#each categories}} {{#each categories}}
<li class='category'> <li class='category'>
{{categoryLink this}} {{categoryLink this allowUncategorized=true}}
<b>{{unbound topic_count}}</b></a> <b>{{unbound topic_count}}</b></a>
</li> </li>
{{/each}} {{/each}}

View File

@ -1,5 +1,10 @@
<div id='list-controls'> <div class='list-controls'>
<div class="container"> <div class="container">
{{#if category}}
{{discourse-breadcrumbs category=category categories=categories}}
{{/if}}
<ul class="nav nav-pills" id='category-filter'> <ul class="nav nav-pills" id='category-filter'>
{{each availableNavItems itemViewClass="Discourse.NavItemView"}} {{each availableNavItems itemViewClass="Discourse.NavItemView"}}
</ul> </ul>

View File

@ -41,9 +41,11 @@
{{/if}} {{/if}}
</td> </td>
{{#unless controller.category}}
<td class='category'> <td class='category'>
{{categoryLink category}} {{categoryLink category}}
</td> </td>
{{/unless}}
<td class='posters'> <td class='posters'>
{{#each posters}} {{#each posters}}

View File

@ -10,10 +10,6 @@
</button> </button>
{{/if}} {{/if}}
{{#if category}}
{{discourse-breadcrumbs title=Discourse.SiteSettings.title category=category categories=categories}}
{{/if}}
<table id='topic-list'> <table id='topic-list'>
<thead> <thead>
<tr> <tr>
@ -23,7 +19,9 @@
<th class='main-link'> <th class='main-link'>
{{i18n topic.title}} {{i18n topic.title}}
</th> </th>
{{#unless category}}
<th class='category'>{{i18n category_title}}</th> <th class='category'>{{i18n category_title}}</th>
{{/unless}}
<th class='posters'>{{i18n top_contributors}}</th> <th class='posters'>{{i18n top_contributors}}</th>
<th class='num posts'>{{i18n posts}}</th> <th class='num posts'>{{i18n posts}}</th>
<th class='num likes'>{{i18n likes}}</th> <th class='num likes'>{{i18n likes}}</th>

View File

@ -22,10 +22,10 @@
{{/if}} {{/if}}
{{categoryLink this allowUncategorized=true}} {{categoryLink this allowUncategorized=true}}
{{#if unreadTopics}} {{#if unreadTopics}}
<a href={{unbound url}} class='badge new-posts badge-notification' title='{{i18n topic.unread_topics count="unreadTopics"}}'>{{unbound unreadTopics}}</a> <a href={{unbound unreadUrl}} class='badge new-posts badge-notification' title='{{i18n topic.unread_topics count="unreadTopics"}}'>{{unbound unreadTopics}}</a>
{{/if}} {{/if}}
{{#if newTopics}} {{#if newTopics}}
<a href={{unbound url}} class='badge new-posts badge-notification' title='{{i18n topic.new_topics count="newTopics"}}'>{{unbound newTopics}} <i class='icon icon-asterisk'></i></a> <a href={{unbound newUrl}} class='badge new-posts badge-notification' title='{{i18n topic.new_topics count="newTopics"}}'>{{unbound newTopics}} <i class='icon icon-asterisk'></i></a>
{{/if}} {{/if}}
<div class='featured-users'> <div class='featured-users'>
{{#each featured_users}} {{#each featured_users}}

View File

@ -21,6 +21,11 @@
{{textField value=name placeholderKey="category.name_placeholder" maxlength="50"}} {{textField value=name placeholderKey="category.name_placeholder" maxlength="50"}}
</section> </section>
<section class='field'>
<label>{{i18n category.parent}}</label>
{{categoryChooser valueAttribute="id" value=parent_category_id categories=parentCategories}}
</section>
{{#unless isUncategorized}} {{#unless isUncategorized}}
<section class='field'> <section class='field'>
<label>{{i18n category.description}}</label> <label>{{i18n category.description}}</label>

View File

@ -1,2 +1,2 @@
<a href="#" class="close" {{action hide target="view"}}><i class="icon icon-remove-sign"></i></a> <span class="close"><i class="icon icon-remove-sign"></i></span>
{{view.validation.reason}} {{view.validation.reason}}

View File

@ -2,6 +2,10 @@
<div class='contents clearfix body-page'> <div class='contents clearfix body-page'>
{{#if content}} {{#if content}}
{{{content}}} {{{content}}}
{{#if showLoginButton}}
<button class="btn btn-primary" {{action showLogin}}>{{i18n log_in}}</button>
{{/if}}
{{else}} {{else}}
<div class='spinner'>{{i18n loading}}</div> <div class='spinner'>{{i18n loading}}</div>
{{/if}} {{/if}}

View File

@ -73,7 +73,7 @@
<section class='controls'> <section class='controls'>
{{#if can_send_private_message_to_user}} {{#if can_send_private_message_to_user}}
<button class='btn btn-primary right' {{action composePrivateMessage}}> <button class='btn btn-primary' {{action composePrivateMessage}}>
<i class='icon icon-envelope'></i> <i class='icon icon-envelope'></i>
{{i18n user.private_message}} {{i18n user.private_message}}
</button> </button>
@ -84,14 +84,14 @@
{{/if}} {{/if}}
{{#if currentUser.staff}} {{#if currentUser.staff}}
<a {{bindAttr href="adminPath"}} class='btn'><i class="icon-wrench"></i>&nbsp;{{i18n admin.user.show_admin_profile}}</a> <a {{bindAttr href="adminPath"}} class='btn right'><i class="icon-wrench"></i>&nbsp;{{i18n admin.user.show_admin_profile}}</a>
{{/if}} {{/if}}
{{#if can_edit}} {{#if can_edit}}
{{#link-to 'preferences' class="btn"}}<i class='icon icon-cog'></i>{{i18n user.preferences}}{{/link-to}} {{#link-to 'preferences' class="btn right"}}<i class='icon icon-cog'></i>{{i18n user.preferences}}{{/link-to}}
{{/if}} {{/if}}
{{#link-to 'user.invited' class="btn"}}<i class='icon icon-envelope-alt'></i>{{i18n user.invited.title}}{{/link-to}} {{#link-to 'user.invited' class="btn right"}}<i class='icon icon-envelope-alt'></i>{{i18n user.invited.title}}{{/link-to}}
</section> </section>
</section> </section>

View File

@ -12,14 +12,16 @@ Discourse.CategoryChooserView = Discourse.ComboboxView.extend({
dataAttributes: ['name', 'color', 'text_color', 'description_text', 'topic_count'], dataAttributes: ['name', 'color', 'text_color', 'description_text', 'topic_count'],
valueBinding: Ember.Binding.oneWay('source'), valueBinding: Ember.Binding.oneWay('source'),
content: Em.computed.filter('categories', function(c) {
var uncategorized_id = Discourse.Site.currentProp("uncategorized_category_id");
return c.get('permission') === Discourse.PermissionType.FULL && c.get('id') !== uncategorized_id;
}),
init: function() { init: function() {
this._super(); this._super();
// TODO perhaps allow passing a param in to select if we need full or not if (!this.get('categories')) {
this.set('categories', Discourse.Category.list());
var uncategorized_id = Discourse.Site.currentProp("uncategorized_category_id"); }
this.set('content', _.filter(Discourse.Category.list(), function(c){
return c.permission === Discourse.PermissionType.FULL && c.id !== uncategorized_id;
}));
}, },
none: function() { none: function() {

View File

@ -27,12 +27,6 @@ Discourse.ModalBodyView = Discourse.View.extend({
} }
}, },
// Pass the errors to our errors view
displayErrors: function(errors, callback) {
this.set('parentView.parentView.modalErrorsView.errors', errors);
if (typeof callback === "function") callback();
},
flashMessageChanged: function() { flashMessageChanged: function() {
var flashMessage = this.get('controller.flashMessage'); var flashMessage = this.get('controller.flashMessage');
if (flashMessage) { if (flashMessage) {

View File

@ -14,7 +14,7 @@ Discourse.NavItemView = Discourse.View.extend({
hidden: Em.computed.not('content.visible'), hidden: Em.computed.not('content.visible'),
count: Ember.computed.alias('content.count'), count: Ember.computed.alias('content.count'),
shouldRerender: Discourse.View.renderIfChanged('count'), shouldRerender: Discourse.View.renderIfChanged('count'),
active: Discourse.computed.propertyEqual('contentNameSlug', 'controller.filterMode'), active: Discourse.computed.propertyEqual('content.filterMode', 'controller.filterMode'),
title: function() { title: function() {
var categoryName, extra, name; var categoryName, extra, name;
@ -27,13 +27,6 @@ Discourse.NavItemView = Discourse.View.extend({
return I18n.t("filters." + name + ".help", extra); return I18n.t("filters." + name + ".help", extra);
}.property("content.filter"), }.property("content.filter"),
contentNameSlug: function() {
return this.get("content.name").toLowerCase().replace(' ','-');
}.property('content.name'),
// active: function() {
// return (this.get("contentNameSlug") === this.get("controller.filterMode"));
// }.property("contentNameSlug", "controller.filterMode"),
name: function() { name: function() {
var categoryName, extra, name; var categoryName, extra, name;

View File

@ -17,6 +17,10 @@ Discourse.PopupInputTipView = Discourse.View.extend({
bouncePixels: 6, bouncePixels: 6,
bounceDelay: 100, bounceDelay: 100,
click: function(e) {
this.set('shownAt', false);
},
good: function() { good: function() {
return !this.get('validation.failed'); return !this.get('validation.failed');
}.property('validation'), }.property('validation'),
@ -25,10 +29,6 @@ Discourse.PopupInputTipView = Discourse.View.extend({
return this.get('validation.failed'); return this.get('validation.failed');
}.property('validation'), }.property('validation'),
hide: function() {
this.set('shownAt', false);
},
bounce: function() { bounce: function() {
if( this.get('shownAt') ) { if( this.get('shownAt') ) {
var $elem = this.$(); var $elem = this.$();

View File

@ -46,7 +46,8 @@ Discourse.QuoteButtonView = Discourse.View.extend({
$(document) $(document)
.on("mousedown.quote-button", function(e) { .on("mousedown.quote-button", function(e) {
view.set('isMouseDown', true); view.set('isMouseDown', true);
if ($(e.target).hasClass('quote-button') || $(e.target).hasClass('create')) return; // we don't want to deselect when we click on the quote button or the reply button
if ($(e.target).hasClass('quote-button') || $(e.target).closest('.create').length > 0) return;
// deselects only when the user left click // deselects only when the user left click
// (allows anyone to `extend` their selection using shift+click) // (allows anyone to `extend` their selection using shift+click)
if (e.which === 1 && !e.shiftKey) controller.deselectText(); if (e.which === 1 && !e.shiftKey) controller.deselectText();

View File

@ -330,7 +330,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
var category = this.get('controller.content.category'); var category = this.get('controller.content.category');
if (category) { if (category) {
opts.catLink = Discourse.Utilities.categoryLink(category); opts.catLink = Discourse.HTML.categoryLink(category);
} else { } else {
opts.catLink = "<a href=\"" + Discourse.getURL("/categories") + "\">" + (I18n.t("topic.browse_all_categories")) + "</a>"; opts.catLink = "<a href=\"" + Discourse.getURL("/categories") + "\">" + (I18n.t("topic.browse_all_categories")) + "</a>";
} }

View File

@ -43,11 +43,11 @@
// Stuff we need to load first // Stuff we need to load first
//= require ./discourse/mixins/scrolling //= require ./discourse/mixins/scrolling
//= require_tree ./discourse/mixins //= require_tree ./discourse/mixins
//= require ./discourse/components/markdown //= require ./discourse/lib/markdown
//= require ./discourse/components/computed //= require ./discourse/lib/computed
//= require ./discourse/views/view //= require ./discourse/views/view
//= require ./discourse/views/container_view //= require ./discourse/views/container_view
//= require ./discourse/components/debounce //= require ./discourse/lib/debounce
//= require ./discourse/models/model //= require ./discourse/models/model
//= require ./discourse/models/user_action //= require ./discourse/models/user_action
//= require ./discourse/models/composer //= require ./discourse/models/composer
@ -63,9 +63,10 @@
//= require ./discourse/dialects/dialect //= require ./discourse/dialects/dialect
//= require_tree ./discourse/dialects //= require_tree ./discourse/dialects
//= require_tree ./discourse/controllers //= require_tree ./discourse/controllers
//= require_tree ./discourse/components //= require_tree ./discourse/lib
//= require_tree ./discourse/models //= require_tree ./discourse/models
//= require_tree ./discourse/views //= require_tree ./discourse/views
//= require_tree ./discourse/components
//= require_tree ./discourse/helpers //= require_tree ./discourse/helpers
//= require_tree ./discourse/templates //= require_tree ./discourse/templates
//= require_tree ./discourse/routes //= require_tree ./discourse/routes

View File

@ -752,6 +752,14 @@ table.api-keys {
} }
} }
.screened-ip-address-form {
margin-left: 6px;
.combobox {
width: 130px;
top: 10px;
}
}
.screened-emails, .screened-urls { .screened-emails, .screened-urls {
.ip_address { .ip_address {
width: 110px; width: 110px;

View File

@ -30,6 +30,7 @@
} }
} }
// Notification badge // Notification badge
// -------------------------------------------------- // --------------------------------------------------

View File

@ -1,5 +1,5 @@
@import "common/foundation/variables"; @import "foundation/variables";
@import "common/foundation/mixins"; @import "foundation/mixins";
.popup-tip { .popup-tip {
position: absolute; position: absolute;
@ -16,14 +16,15 @@
&.hide, &.good { &.hide, &.good {
display: none; display: none;
} }
a.close { .close {
float: right; float: right;
color: $black; color: $black;
opacity: 0.5; opacity: 0.5;
font-size: 15px; font-size: 15px;
margin-left: 4px; margin-left: 4px;
cursor: pointer;
} }
a.close:hover { .close:hover {
opacity: 1.0; opacity: 1.0;
} }
} }

View File

@ -164,6 +164,19 @@
border-bottom: 1px solid #bbb; border-bottom: 1px solid #bbb;
} }
.category-combobox {
width: 430px;
.chzn-drop {
left: -9000px;
width: 428px;
}
.chzn-search input {
width: 378px;
}
}
&.hidden { &.hidden {
display: none; display: none;
} }

View File

@ -8,7 +8,7 @@
// List controls // List controls
// -------------------------------------------------- // --------------------------------------------------
#list-controls { .list-controls {
.nav { .nav {
float: left; float: left;
margin-bottom: 15px; margin-bottom: 15px;
@ -340,52 +340,102 @@
// Misc. stuff // Misc. stuff
// -------------------------------------------------- // --------------------------------------------------
#main { .list-controls {
#list-controls { .home {
.badge-category { font-size: 20px;
display: inline-block; font-weight: normal;
background-color: yellow;
margin: 8px 0 0 8px;
float: left;
}
clear: both;
} }
#list-area {
margin-bottom: 300px; .badge-category {
padding: 4px 10px;
display: inline-block;
line-height: 24px;
float: left;
}
.category-dropdown-button {
padding: 4px 10px 3px 8px;
border-left: 1px solid rgba(0, 0, 0, 0.15);
font-size: 16px;
width: 10px;
text-align: center;
&:hover {
opacity: 0.8;
}
}
clear: both;
}
#list-area {
margin-bottom: 300px;
.topic-statuses .topic-status i {font-size: 15px;} .topic-statuses .topic-status i {font-size: 15px;}
.empty-topic-list {
padding: 10px;
.empty-topic-list {
padding: 10px;
}
.unseen {
background-color: transparent;
padding: 0;
border: 0;
color: lighten($red, 10%);
font-size: 13px;
cursor: default;
}
} }
#topic-list { .unseen {
.alert { background-color: transparent;
margin-bottom: 0; padding: 0;
font-size: 14px; border: 0;
} color: lighten($red, 10%);
.spinner { font-size: 13px;
margin-top: 40px; cursor: default;
}
}
span.posted {
display: inline-block;
text-indent: -9999em;
width: 15px;
height: 15px;
background: {
image: image-url("posted.png");
};
} }
} }
#topic-list {
.alert {
margin-bottom: 0;
font-size: 14px;
}
.spinner {
margin-top: 40px;
}
}
span.posted {
display: inline-block;
text-indent: -9999em;
width: 15px;
height: 15px;
background: {
image: image-url("posted.png");
};
}
ol.category-breadcrumb {
display: block;
float: left;
list-style: none;
margin: 0 10px 0 0;
padding: 0;
li {
float: left;
margin-right: 5px;
}
}
.category-dropdown-menu {
overflow-x: hidden;
overflow-y: scroll;
position: absolute;
border: 1px solid #ccc;
background-color: white;
width: 200px;
height: 200px;
padding: 8px 5px 0 7px;
z-index: 100;
margin-top: 31px;
.badge-category {
font-size: 13px;
line-height: 26px;
padding: 0px 8px;
float: none;
}
div {
margin-bottom: 10px;
}
}

View File

@ -198,45 +198,4 @@ i {background: #e4f2f8;
z-index: 495 z-index: 495
} }
ol.category-breadcrumb {
list-style: none;
margin: 0 0 10px 0;
padding: 0;
.first-caret {
margin: 0 3px 0 3px;
color: #666;
font-size: 13px;
}
li {
float: left;
margin-right: 5px;
border: 1px solid transparent;
height: 21px;
padding-top: 2px;
button {
background: none;
border: 1px solid transparent;
padding: 0px 4px;
margin: 0;
color: #666;
}
&.has-drop:hover {
background-color: #eee;
border: 1px solid #bbb;
button {
color: black;
}
}
}
.clear {
clear: both;
}
}

View File

@ -205,9 +205,47 @@
background-color: #ddd; background-color: #ddd;
margin-top: 10px; margin-top: 10px;
padding: 5px; padding: 5px;
height: 32px;
.right { .right {
float: right; float: right;
margin-left: 5px;
}
}
}
.about.collapsed-info {
.controls {
margin-top: 0;
}
.details {
.secondary { display: none; }
.bio { display: none; }
.primary {
width: 100%;
text-align: left;
margin-top: 0;
.avatar {
float: left;
margin-right: 10px;
border: 2px solid white;
width: 45px;
height: 45px;
}
h1 {
font-size: 20px;
line-height: 22px;
}
h2 {
font-size: 17px;
line-height: 20px;
margin-top: 4px;
}
} }
} }
} }

View File

@ -279,14 +279,15 @@ display: none;
display: inline; display: inline;
} }
.title-input .popup-tip { .title-input .popup-tip {
width: 300px; width: 240px;
left: -8px; right: 5px;
margin-top: 8px;
} }
.category-input .popup-tip { .category-input .popup-tip {
width: 240px; width: 240px;
left: 432px; right: 5px;
top: -7px; }
.textarea-wrapper .popup-tip {
top: 28px;
} }
button.btn.no-text { button.btn.no-text {
margin: 7px 0 0 5px; margin: 7px 0 0 5px;

View File

@ -14,6 +14,7 @@
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 16px; font-size: 16px;
line-height: 22px; line-height: 22px;
margin: 20px 15px;
// Consistent vertical spacing // Consistent vertical spacing
blockquote, blockquote,

View File

@ -1,29 +0,0 @@
@import "../common/foundation/variables";
@import "../common/foundation/mixins";
.popup-tip {
position: absolute;
display: block;
padding: 5px 10px;
z-index: 101;
@include border-radius-all(2px);
border: solid 1px #955;
&.bad {
background-color: #b66;
color: white;
box-shadow: 1px 1px 5px #777, inset 0 0 9px #b55;
}
&.hide, &.good {
display: none;
}
a.close {
float: right;
color: $black;
opacity: 0.5;
font-size: 15px;
margin-left: 4px;
}
a.close:hover {
opacity: 1.0;
}
}

View File

@ -8,7 +8,7 @@
// List controls // List controls
// -------------------------------------------------- // --------------------------------------------------
#list-controls { .list-controls {
margin: 5px; margin: 5px;
.nav { .nav {
float: left; float: left;
@ -239,48 +239,64 @@
// Misc. stuff // Misc. stuff
// -------------------------------------------------- // --------------------------------------------------
#main { .list-controls {
#list-controls { .home {
.badge-category { font-size: 20px;
display: inline-block; font-weight: normal;
background-color: yellow;
margin: 8px 0 0 8px;
float: left;
}
clear: both;
} }
#list-area {
margin-bottom: 300px; .badge-category {
.empty-topic-list { margin-top: 6px;
padding: 10px; padding: 4px 10px;
}
.unseen {
background-color: transparent;
padding: 0;
border: 0;
color: lighten($red, 10%);
font-size: 13px;
cursor: default;
}
}
#topic-list {
.alert {
margin-bottom: 0;
font-size: 14px;
}
.spinner {
margin-top: 40px;
}
}
span.posted {
display: inline-block; display: inline-block;
text-indent: -9999em; line-height: 24px;
width: 15px; float: left;
height: 15px;
background: {
image: image-url("posted.png");
};
} }
.category-dropdown-button {
padding: 4px 10px 3px 8px;
border-left: 1px solid rgba(0, 0, 0, 0.15);
font-size: 16px;
width: 10px;
text-align: center;
&:hover {
opacity: 0.8;
}
}
clear: both;
}
#list-area {
margin-bottom: 300px;
.empty-topic-list {
padding: 10px;
}
.unseen {
background-color: transparent;
padding: 0;
border: 0;
color: lighten($red, 10%);
font-size: 13px;
cursor: default;
}
}
#topic-list {
.alert {
margin-bottom: 0;
font-size: 14px;
}
.spinner {
margin-top: 40px;
}
}
span.posted {
display: inline-block;
text-indent: -9999em;
width: 15px;
height: 15px;
background: {
image: image-url("posted.png");
};
} }
@ -364,3 +380,40 @@ clear: both;
opacity: 1; opacity: 1;
} }
ol.category-breadcrumb {
display: block;
float: left;
list-style: none;
margin: 0 10px 0 0;
padding: 0;
li {
float: left;
margin-right: 5px;
}
}
.category-dropdown-menu {
overflow-x: hidden;
overflow-y: scroll;
position: absolute;
border: 1px solid #ccc;
background-color: white;
width: 200px;
height: 200px;
padding: 8px 5px 0 7px;
z-index: 100;
margin-top: 31px;
.badge-category {
font-size: 13px;
line-height: 26px;
padding: 0px 8px;
float: none;
}
div {
margin-bottom: 10px;
}
}

View File

@ -7,6 +7,15 @@ class Admin::ScreenedIpAddressesController < Admin::AdminController
render_serialized(screened_ip_addresses, ScreenedIpAddressSerializer) render_serialized(screened_ip_addresses, ScreenedIpAddressSerializer)
end end
def create
screened_ip_address = ScreenedIpAddress.new(allowed_params)
if screened_ip_address.save
render_serialized(screened_ip_address, ScreenedIpAddressSerializer)
else
render_json_error(screened_ip_address)
end
end
def update def update
if @screened_ip_address.update_attributes(allowed_params) if @screened_ip_address.update_attributes(allowed_params)
render json: success_json render json: success_json

View File

@ -90,7 +90,7 @@ class CategoriesController < ApplicationController
end end
end end
params.permit(*required_param_keys, :position, :hotness, :auto_close_days, :permissions => [*p.try(:keys)]) params.permit(*required_param_keys, :position, :hotness, :parent_category_id, :auto_close_days, :permissions => [*p.try(:keys)])
end end
end end

View File

@ -10,14 +10,11 @@ class SessionController < ApplicationController
params.require(:login) params.require(:login)
params.require(:password) params.require(:password)
login = params[:login].strip login = params[:login].strip
login = login[1..-1] if login[0] == "@" password = params[:password]
login = login[1..-1] if login[0] == "@"
if login =~ /@/ @user = User.find_by_username_or_email(login)
@user = User.where(email: Email.downcase(login)).first
else
@user = User.where(username_lower: login.downcase).first
end
if @user.present? if @user.present?
@ -28,7 +25,7 @@ class SessionController < ApplicationController
end end
# If their password is correct # If their password is correct
if @user.confirm_password?(params[:password]) if @user.confirm_password?(password)
if @user.is_banned? if @user.is_banned?
render json: { error: I18n.t("login.banned", {date: I18n.l(@user.banned_till, format: :date_only)}) } render json: { error: I18n.t("login.banned", {date: I18n.l(@user.banned_till, format: :date_only)}) }
@ -57,7 +54,7 @@ class SessionController < ApplicationController
def forgot_password def forgot_password
params.require(:login) params.require(:login)
user = User.where('username_lower = :username or email = :email', username: params[:login].downcase, email: Email.downcase(params[:login])).first user = User.find_by_username_or_email(params[:login])
if user.present? if user.present?
email_token = user.email_tokens.create(email: user.email) email_token = user.email_tokens.create(email: user.email)
Jobs.enqueue(:user_email, type: :forgot_password, user_id: user.id, email_token: email_token.token) Jobs.enqueue(:user_email, type: :forgot_password, user_id: user.id, email_token: email_token.token)

View File

@ -29,8 +29,9 @@ class TopicsController < ApplicationController
return wordpress if params[:best].present? return wordpress if params[:best].present?
opts = params.slice(:username_filters, :filter, :page, :post_number) opts = params.slice(:username_filters, :filter, :page, :post_number)
username_filters = opts[:username_filters]
opts[:username_filters] = [opts[:username_filters]] if opts[:username_filters].is_a?(String) opts[:username_filters] = [username_filters] if username_filters.is_a?(String)
begin begin
@topic_view = TopicView.new(params[:id] || params[:topic_id], current_user, opts) @topic_view = TopicView.new(params[:id] || params[:topic_id], current_user, opts)
@ -46,7 +47,7 @@ class TopicsController < ApplicationController
# render workaround pseudo-static HTML page for old crawlers which ignores <noscript> # render workaround pseudo-static HTML page for old crawlers which ignores <noscript>
# (see http://meta.discourse.org/t/noscript-tag-and-some-search-engines/8078) # (see http://meta.discourse.org/t/noscript-tag-and-some-search-engines/8078)
return render 'topics/plain', layout: false if (SiteSetting.enable_escaped_fragments && params.has_key?('_escaped_fragment_')) return render 'topics/plain', layout: false if (SiteSetting.enable_escaped_fragments && params.key?('_escaped_fragment_'))
track_visit_to_topic track_visit_to_topic
@ -63,25 +64,21 @@ class TopicsController < ApplicationController
params.require(:best) params.require(:best)
params.require(:topic_id) params.require(:topic_id)
params.permit(:min_trust_level, :min_score, :min_replies, :bypass_trust_level_score, :only_moderator_liked) params.permit(:min_trust_level, :min_score, :min_replies, :bypass_trust_level_score, :only_moderator_liked)
opts = { best: params[:best].to_i,
min_trust_level: params[:min_trust_level] ? 1 : params[:min_trust_level].to_i,
min_score: params[:min_score].to_i,
min_replies: params[:min_replies].to_i,
bypass_trust_level_score: params[:bypass_trust_level_score].to_i, # safe cause 0 means ignore
only_moderator_liked: params[:only_moderator_liked].to_s == "true"
}
@topic_view = TopicView.new( @topic_view = TopicView.new(params[:topic_id], current_user, opts)
params[:topic_id],
current_user,
best: params[:best].to_i,
min_trust_level: params[:min_trust_level].nil? ? 1 : params[:min_trust_level].to_i,
min_score: params[:min_score].to_i,
min_replies: params[:min_replies].to_i,
bypass_trust_level_score: params[:bypass_trust_level_score].to_i, # safe cause 0 means ignore
only_moderator_liked: params[:only_moderator_liked].to_s == "true"
)
discourse_expires_in 1.minute discourse_expires_in 1.minute
wordpress_serializer = TopicViewWordpressSerializer.new(@topic_view, scope: guardian, root: false) wordpress_serializer = TopicViewWordpressSerializer.new(@topic_view, scope: guardian, root: false)
render_json_dump(wordpress_serializer) render_json_dump(wordpress_serializer)
end end
def posts def posts
params.require(:topic_id) params.require(:topic_id)
params.require(:post_ids) params.require(:post_ids)
@ -97,41 +94,31 @@ class TopicsController < ApplicationController
def update def update
topic = Topic.where(id: params[:topic_id]).first topic = Topic.where(id: params[:topic_id]).first
title, archetype = params[:title], params[:archetype]
guardian.ensure_can_edit!(topic) guardian.ensure_can_edit!(topic)
topic.title = params[:title] if params[:title].present?
topic.title = params[:title] if title.present?
# TODO: we may need smarter rules about converting archetypes # TODO: we may need smarter rules about converting archetypes
if current_user.admin? topic.archetype = "regular" if current_user.admin? && archetype == 'regular'
topic.archetype = "regular" if params[:archetype] == 'regular'
end
success = false success = false
Topic.transaction do Topic.transaction do
success = topic.save success = topic.save
success = topic.change_category(params[:category]) if success success = topic.change_category(params[:category]) if success
end end
# this is used to return the title to the client as it may have been # this is used to return the title to the client as it may have been
# changed by "TextCleaner" # changed by "TextCleaner"
if success success ? render_serialized(topic, BasicTopicSerializer) : render_json_error(topic)
render_serialized(topic, BasicTopicSerializer)
else
render_json_error(topic)
end
end end
def similar_to def similar_to
params.require(:title) params.require(:title)
params.require(:raw) params.require(:raw)
title, raw = params[:title], params[:raw] title, raw = params[:title], params[:raw]
[:title, :raw].each { |key| check_length_of(key, params[key]) }
raise Discourse::InvalidParameters.new(:title) if title.length < SiteSetting.min_title_similar_length
raise Discourse::InvalidParameters.new(:raw) if raw.length < SiteSetting.min_body_similar_length
# Only suggest similar topics if the site has a minimmum amount of topics present. # Only suggest similar topics if the site has a minimmum amount of topics present.
if Topic.count > SiteSetting.minimum_topics_similar topics = Topic.similar_to(title, raw, current_user).to_a if Topic.count_exceeds_minimum?
topics = Topic.similar_to(title, raw, current_user).to_a
end
render_serialized(topics, BasicTopicSerializer) render_serialized(topics, BasicTopicSerializer)
end end
@ -139,11 +126,13 @@ class TopicsController < ApplicationController
def status def status
params.require(:status) params.require(:status)
params.require(:enabled) params.require(:enabled)
status, topic_id = params[:status], params[:topic_id].to_i
enabled = (params[:enabled] == 'true')
raise Discourse::InvalidParameters.new(:status) unless %w(visible closed pinned archived).include?(params[:status]) check_for_status_presence(:status, status)
@topic = Topic.where(id: params[:topic_id].to_i).first @topic = Topic.where(id: topic_id).first
guardian.ensure_can_moderate!(@topic) guardian.ensure_can_moderate!(@topic)
@topic.update_status(params[:status], (params[:enabled] == 'true'), current_user) @topic.update_status(status, enabled, current_user)
render nothing: true render nothing: true
end end
@ -203,14 +192,7 @@ class TopicsController < ApplicationController
end end
def invite def invite
username_or_email = params[:user] username_or_email = params[:user] ? fetch_username : fetch_email
if username_or_email
# provides a level of protection for hashes
params.require(:user)
else
params.require(:email)
username_or_email = params[:email]
end
topic = Topic.where(id: params[:topic_id]).first topic = Topic.where(id: params[:topic_id]).first
guardian.ensure_can_invite_to!(topic) guardian.ensure_can_invite_to!(topic)
@ -347,4 +329,27 @@ class TopicsController < ApplicationController
topic.move_posts(current_user, post_ids_including_replies, args) topic.move_posts(current_user, post_ids_including_replies, args)
end end
def check_length_of(key, attr)
str = (key == :raw) ? "body" : key.to_s
invalid_param(key) if attr.length < SiteSetting.send("min_#{str}_similar_length")
end
def check_for_status_presence(key, attr)
invalid_param(key) unless %w(visible closed pinned archived).include?(attr)
end
def invalid_param(key)
raise Discourse::InvalidParameters.new(key.to_sym)
end
def fetch_username
params.require(:user)
params[:user]
end
def fetch_email
params.require(:email)
params[:email]
end
end end

View File

@ -22,6 +22,7 @@ class AdminDashboardData
def problems def problems
[ rails_env_check, [ rails_env_check,
ruby_version_check,
host_names_check, host_names_check,
gc_checks, gc_checks,
sidekiq_check || queue_size_check, sidekiq_check || queue_size_check,
@ -161,6 +162,10 @@ class AdminDashboardData
I18n.t('dashboard.notification_email_warning') if SiteSetting.notification_email.blank? I18n.t('dashboard.notification_email_warning') if SiteSetting.notification_email.blank?
end end
def ruby_version_check
I18n.t('dashboard.ruby_version_warning') if RUBY_VERSION == '2.0.0' and RUBY_PATCHLEVEL < 247
end
# TODO: generalize this method of putting i18n keys with expiry in redis # TODO: generalize this method of putting i18n keys with expiry in redis
# that should be reported on the admin dashboard: # that should be reported on the admin dashboard:

View File

@ -187,10 +187,10 @@ SQL
def parent_category_validator def parent_category_validator
if parent_category_id if parent_category_id
errors.add(:parent_category_id, "You can't link a category to itself") if parent_category_id == id errors.add(:parent_category_id, I18n.t("category.errors.self_parent")) if parent_category_id == id
grandfather_id = Category.where(id: parent_category_id).pluck(:parent_category_id).first grandfather_id = Category.where(id: parent_category_id).pluck(:parent_category_id).first
errors.add(:parent_category_id, "You can't have more than one level of subcategory") if grandfather_id errors.add(:base, I18n.t("category.errors.depth")) if grandfather_id
end end
end end

View File

@ -59,7 +59,8 @@ class Topic < ActiveRecord::Base
validates :category_id, :presence => true ,:exclusion => {:in => [SiteSetting.uncategorized_category_id]}, validates :category_id, :presence => true ,:exclusion => {:in => [SiteSetting.uncategorized_category_id]},
:if => Proc.new { |t| :if => Proc.new { |t|
(t.new_record? || t.category_id_changed?) && (t.new_record? || t.category_id_changed?) &&
!SiteSetting.allow_uncategorized_topics !SiteSetting.allow_uncategorized_topics &&
(t.archetype.nil? || t.archetype == Archetype.default)
} }
@ -106,7 +107,7 @@ class Topic < ActiveRecord::Base
# Return private message topics # Return private message topics
scope :private_messages, lambda { scope :private_messages, lambda {
where(archetype: Archetype::private_message) where(archetype: Archetype.private_message)
} }
scope :listable_topics, lambda { where('topics.archetype <> ?', [Archetype.private_message]) } scope :listable_topics, lambda { where('topics.archetype <> ?', [Archetype.private_message]) }
@ -169,7 +170,7 @@ class Topic < ActiveRecord::Base
Jobs.cancel_scheduled_job(:close_topic, {topic_id: id}) Jobs.cancel_scheduled_job(:close_topic, {topic_id: id})
true true
end end
if category_id.nil? && (archetype.nil? || archetype == "regular") if category_id.nil? && (archetype.nil? || archetype == Archetype.default)
self.category_id = SiteSetting.uncategorized_category_id self.category_id = SiteSetting.uncategorized_category_id
end end
end end
@ -182,6 +183,10 @@ class Topic < ActiveRecord::Base
end end
end end
def self.count_exceeds_minimum?
count > SiteSetting.minimum_topics_similar
end
def best_post def best_post
posts.order('score desc').limit(1).first posts.order('score desc').limit(1).first
end end

View File

@ -124,19 +124,19 @@ class User < ActiveRecord::Base
end end
def self.find_by_username_or_email(username_or_email) def self.find_by_username_or_email(username_or_email)
conditions = if username_or_email.include?('@') if username_or_email.include?('@')
{ email: Email.downcase(username_or_email) } find_by_email(username_or_email)
else else
{ username_lower: username_or_email.downcase } find_by_username(username_or_email)
end end
end
users = User.where(conditions).to_a def self.find_by_email(email)
where(email: Email.downcase(email)).first
end
if users.size > 1 def self.find_by_username(username)
raise Discourse::TooManyMatches where(username_lower: username.downcase).first
else
users.first
end
end end
def enqueue_welcome_message(message_type) def enqueue_welcome_message(message_type)

View File

@ -1,12 +1,12 @@
class ScreenedIpAddressSerializer < ApplicationSerializer class ScreenedIpAddressSerializer < ApplicationSerializer
attributes :id, attributes :id,
:ip_address, :ip_address,
:action, :action_name,
:match_count, :match_count,
:last_match_at, :last_match_at,
:created_at :created_at
def action def action_name
ScreenedIpAddress.actions.key(object.action_type).to_s ScreenedIpAddress.actions.key(object.action_type).to_s
end end

View File

@ -0,0 +1,45 @@
class SpamRule::AutoBlock
def initialize(user)
@user = user
end
def self.block?(user)
self.new(user).block?
end
def self.punish!(user)
self.new(user).block_user
end
def perform
block_user if block?
end
def block?
@user.blocked? or
(!@user.has_trust_level?(:basic) and
SiteSetting.num_flags_to_block_new_user > 0 and
SiteSetting.num_users_to_block_new_user > 0 and
num_spam_flags_against_user >= SiteSetting.num_flags_to_block_new_user and
num_users_who_flagged_spam_against_user >= SiteSetting.num_users_to_block_new_user)
end
def num_spam_flags_against_user
Post.where(user_id: @user.id).sum(:spam_count)
end
def num_users_who_flagged_spam_against_user
post_ids = Post.where('user_id = ? and spam_count > 0', @user.id).pluck(:id)
return 0 if post_ids.empty?
PostAction.spam_flags.where(post_id: post_ids).uniq.pluck(:user_id).size
end
def block_user
Post.transaction do
if UserBlocker.block(@user, nil, {message: :too_many_spam_flags}) and SiteSetting.notify_mods_when_user_blocked
GroupMessage.create(Group[:moderators].name, :user_automatically_blocked, {user: @user, limit_once_per: false})
end
end
end
end

View File

@ -0,0 +1,37 @@
class SpamRule::FlagSockpuppets
def initialize(post)
@post = post
end
def perform
if SiteSetting.flag_sockpuppets and reply_is_from_sockpuppet?
flag_sockpuppet_users
true
else
false
end
end
def reply_is_from_sockpuppet?
return false if @post.post_number and @post.post_number == 1
first_post = @post.topic.posts.by_post_number.first
return false if first_post.user.nil?
!first_post.user.staff? and !@post.user.staff? and
@post.user != first_post.user and
@post.user.ip_address == first_post.user.ip_address and
@post.user.new_user? and
!ScreenedIpAddress.is_whitelisted?(@post.user.ip_address)
end
def flag_sockpuppet_users
system_user = Discourse.system_user
PostAction.act(system_user, @post, PostActionType.types[:spam], message: I18n.t('flag_reason.sockpuppet')) rescue PostAction::AlreadyActed
if (first_post = @post.topic.posts.by_post_number.first).try(:user).try(:new_user?)
PostAction.act(system_user, first_post, PostActionType.types[:spam], message: I18n.t('flag_reason.sockpuppet')) rescue PostAction::AlreadyActed
end
end
end

View File

@ -2,8 +2,6 @@
# receive, their trust level, etc. # receive, their trust level, etc.
class SpamRulesEnforcer class SpamRulesEnforcer
include Rails.application.routes.url_helpers
# The exclamation point means that this method may make big changes to posts and users. # The exclamation point means that this method may make big changes to posts and users.
def self.enforce!(arg) def self.enforce!(arg)
SpamRulesEnforcer.new(arg).enforce! SpamRulesEnforcer.new(arg).enforce!
@ -15,73 +13,13 @@ class SpamRulesEnforcer
end end
def enforce! def enforce!
# TODO: once rules are in their own classes, invoke them from here in priority order
if @user if @user
block_user if block? SpamRule::AutoBlock.new(@user).perform
end end
if @post if @post
flag_sockpuppet_users if SiteSetting.flag_sockpuppets and reply_is_from_sockpuppet? SpamRule::FlagSockpuppets.new(@post).perform
end end
true true
end end
# TODO: move this sockpuppet code to its own class. We should be able to add more rules, like ActiveModel validators.
def reply_is_from_sockpuppet?
return false if @post.post_number and @post.post_number == 1
first_post = @post.topic.posts.by_post_number.first
return false if first_post.user.nil?
!first_post.user.staff? and !@post.user.staff? and
@post.user != first_post.user and
@post.user.ip_address == first_post.user.ip_address and
@post.user.new_user? and
!ScreenedIpAddress.is_whitelisted?(@post.user.ip_address)
end
def flag_sockpuppet_users
system_user = Discourse.system_user
PostAction.act(system_user, @post, PostActionType.types[:spam], message: I18n.t('flag_reason.sockpuppet')) rescue PostAction::AlreadyActed
if (first_post = @post.topic.posts.by_post_number.first).try(:user).try(:new_user?)
PostAction.act(system_user, first_post, PostActionType.types[:spam], message: I18n.t('flag_reason.sockpuppet')) rescue PostAction::AlreadyActed
end
end
# TODO: move all this auto-block code to another class:
def self.block?(user)
SpamRulesEnforcer.new(user).block?
end
def self.punish!(user)
SpamRulesEnforcer.new(user).block_user
end
def block?
@user.blocked? or
(!@user.has_trust_level?(:basic) and
SiteSetting.num_flags_to_block_new_user > 0 and
SiteSetting.num_users_to_block_new_user > 0 and
num_spam_flags_against_user >= SiteSetting.num_flags_to_block_new_user and
num_users_who_flagged_spam_against_user >= SiteSetting.num_users_to_block_new_user)
end
def num_spam_flags_against_user
Post.where(user_id: @user.id).sum(:spam_count)
end
def num_users_who_flagged_spam_against_user
post_ids = Post.where('user_id = ? and spam_count > 0', @user.id).pluck(:id)
return 0 if post_ids.empty?
PostAction.spam_flags.where(post_id: post_ids).uniq.pluck(:user_id).size
end
def block_user
Post.transaction do
if UserBlocker.block(@user, nil, {message: :too_many_spam_flags}) and SiteSetting.notify_mods_when_user_blocked
GroupMessage.create(Group[:moderators].name, :user_automatically_blocked, {user: @user, limit_once_per: false})
end
end
end
end end

View File

@ -18,6 +18,14 @@ module Discourse
# Application configuration should go into files in config/initializers # Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded. # -- all .rb files in that directory are automatically loaded.
# HACK!! regression in rubygems / bundler in ruby-head
if RUBY_VERSION == "2.1.0"
$:.map! do |path|
path = File.expand_path(path.sub("../../","../")) if path =~ /fast_xor/ && !File.directory?(File.expand_path(path))
path
end
end
require 'discourse' require 'discourse'
require 'js_locale_helper' require 'js_locale_helper'

View File

@ -1,7 +1,7 @@
production: production:
first_thing: first_thing:
# 1. Permissions on postgres box # 1. Permissions on postgres box
- source: /.cloud66/scripts/permissions.sh - source: /config/cloud/cloud66/scripts/permissions.sh
destination: /tmp/scripts/permissions.sh destination: /tmp/scripts/permissions.sh
target: postgresql target: postgresql
apply_during: build_only apply_during: build_only
@ -16,25 +16,29 @@ production:
owner: postgres owner: postgres
after_checkout: after_checkout:
# 3. Copy Procfile # 3. Copy Procfile
- source: /.cloud66/files/Procfile - source: /config/cloud/cloud66/files/Procfile
destination: <%= ENV['RAILS_STACK_PATH'] %>/Procfile destination: <%= ENV['RAILS_STACK_PATH'] %>/Procfile
target: rails target: rails
run_on: all_servers
# 4. Copy redis settings # 4. Copy redis settings
- source: /.cloud66/files/redis.yml - source: /config/cloud/cloud66/files/redis.yml
destination: <%= ENV['RAILS_STACK_PATH'] %>/config/redis.yml destination: <%= ENV['RAILS_STACK_PATH'] %>/config/redis.yml
target: rails target: rails
parse: false parse: false
run_on: all_servers
# 5. Copy production.rb file # 5. Copy production.rb file
- source: /.cloud66/files/production.rb - source: /config/cloud/cloud66/files/production.rb
destination: <%= ENV['RAILS_STACK_PATH'] %>/config/environments/production.rb destination: <%= ENV['RAILS_STACK_PATH'] %>/config/environments/production.rb
target: rails target: rails
run_on: all_servers
# 6. Move thin config to server # 6. Move thin config to server
- source: /.cloud66/files/thin.yml - source: /config/cloud/cloud66/files/thin.yml
destination: <%= ENV['RAILS_STACK_PATH'] %>/config/thin.yml destination: <%= ENV['RAILS_STACK_PATH'] %>/config/thin.yml
target: rails target: rails
run_on: all_servers
after_rails: after_rails:
# 7. Set environment variables and allow PSQL user to access them # 7. Set environment variables and allow PSQL user to access them
- source: /.cloud66/scripts/env_vars.sh - source: /config/cloud/cloud66/scripts/env_vars.sh
destination: /tmp/scripts/env_vars.sh destination: /tmp/scripts/env_vars.sh
target: rails target: rails
apply_during: build_only apply_during: build_only
@ -42,21 +46,21 @@ production:
sudo: true sudo: true
last_thing: last_thing:
# 8. KILL DB # 8. KILL DB
- source: /.cloud66/scripts/kill_db.sh - source: /config/cloud/cloud66/scripts/kill_db.sh
destination: /tmp/scripts/kill_db.sh destination: /tmp/scripts/kill_db.sh
target: postgresql target: postgresql
apply_during: build_only apply_during: build_only
execute: true execute: true
sudo: true sudo: true
# 9. DB:DROP & DB:CREATE # 9. DB:DROP & DB:CREATE
- source: /.cloud66/scripts/drop_create.sh - source: /config/cloud/cloud66/scripts/drop_create.sh
destination: /tmp/scripts/drop_create.sh destination: /tmp/scripts/drop_create.sh
target: rails target: rails
apply_during: build_only apply_during: build_only
execute: true execute: true
sudo: true sudo: true
# 10. Import database image # 10. Import database image
- source: /.cloud66/scripts/import_prod.sh - source: /config/cloud/cloud66/scripts/import_prod.sh
destination: /tmp/scripts/import_prod.sh destination: /tmp/scripts/import_prod.sh
target: postgresql target: postgresql
apply_during: build_only apply_during: build_only
@ -64,14 +68,14 @@ production:
owner: postgres owner: postgres
run_as: postgres run_as: postgres
# 11. Migrate database # 11. Migrate database
- source: /.cloud66/scripts/migrate.sh - source: /config/cloud/cloud66/scripts/migrate.sh
destination: /tmp/migrate.sh destination: /tmp/migrate.sh
target: rails target: rails
apply_during: build_only apply_during: build_only
execute: true execute: true
sudo: true sudo: true
# 12. Curl script # 12. Curl script
- source: /.cloud66/scripts/curl.sh - source: /config/cloud/cloud66/scripts/curl.sh
destination: /tmp/curl.sh destination: /tmp/curl.sh
target: rails target: rails
apply_during: build_only apply_during: build_only
@ -80,7 +84,7 @@ production:
staging: staging:
first_thing: first_thing:
# 1. Permissions on postgres box # 1. Permissions on postgres box
- source: /.cloud66/scripts/permissions.sh - source: /config/cloud/cloud66/scripts/permissions.sh
destination: /tmp/scripts/permissions.sh destination: /tmp/scripts/permissions.sh
target: postgresql target: postgresql
apply_during: build_only apply_during: build_only
@ -95,25 +99,29 @@ staging:
owner: postgres owner: postgres
after_checkout: after_checkout:
# 3. Copy Procfile # 3. Copy Procfile
- source: /.cloud66/files/Procfile - source: /config/cloud/cloud66/files/Procfile
destination: <%= ENV['RAILS_STACK_PATH'] %>/Procfile destination: <%= ENV['RAILS_STACK_PATH'] %>/Procfile
target: rails target: rails
run_on: all_servers
# 4. Rename redis.yml.sample file # 4. Rename redis.yml.sample file
- source: /.cloud66/files/redis.yml - source: /config/cloud/cloud66/files/redis.yml
destination: <%= ENV['RAILS_STACK_PATH'] %>/config/redis.yml destination: <%= ENV['RAILS_STACK_PATH'] %>/config/redis.yml
target: rails target: rails
parse: false parse: false
run_on: all_servers
# 5. Rename production.rb.sample file # 5. Rename production.rb.sample file
- source: /.cloud66/files/production.rb - source: /config/cloud/cloud66/files/production.rb
destination: <%= ENV['RAILS_STACK_PATH'] %>/config/environments/production.rb destination: <%= ENV['RAILS_STACK_PATH'] %>/config/environments/production.rb
target: rails target: rails
run_on: all_servers
# 6. Move thin config to server # 6. Move thin config to server
- source: /.cloud66/files/thin.yml - source: /config/cloud/cloud66/files/thin.yml
destination: <%= ENV['RAILS_STACK_PATH'] %>/config/thin.yml destination: <%= ENV['RAILS_STACK_PATH'] %>/config/thin.yml
target: rails target: rails
run_on: all_servers
after_rails: after_rails:
# 7. Set environment variables and allow PSQL user to access them # 7. Set environment variables and allow PSQL user to access them
- source: /.cloud66/scripts/env_vars.sh - source: /config/cloud/cloud66/scripts/env_vars.sh
destination: /tmp/scripts/env_vars.sh destination: /tmp/scripts/env_vars.sh
target: rails target: rails
apply_during: build_only apply_during: build_only
@ -121,21 +129,21 @@ staging:
sudo: true sudo: true
last_thing: last_thing:
# 8. KILL DB # 8. KILL DB
- source: /.cloud66/scripts/kill_db.sh - source: /config/cloud/cloud66/scripts/kill_db.sh
destination: /tmp/scripts/kill_db.sh destination: /tmp/scripts/kill_db.sh
target: postgresql target: postgresql
apply_during: build_only apply_during: build_only
execute: true execute: true
sudo: true sudo: true
# 9. DB:DROP & DB:CREATE # 9. DB:DROP & DB:CREATE
- source: /.cloud66/scripts/drop_create.sh - source: /config/cloud/cloud66/scripts/drop_create.sh
destination: /tmp/scripts/drop_create.sh destination: /tmp/scripts/drop_create.sh
target: rails target: rails
apply_during: build_only apply_during: build_only
execute: true execute: true
sudo: true sudo: true
# 10. Import database image # 10. Import database image
- source: /.cloud66/scripts/import_prod.sh - source: /config/cloud/cloud66/scripts/import_prod.sh
destination: /tmp/scripts/import_prod.sh destination: /tmp/scripts/import_prod.sh
target: postgresql target: postgresql
apply_during: build_only apply_during: build_only
@ -143,14 +151,14 @@ staging:
owner: postgres owner: postgres
run_as: postgres run_as: postgres
# 11. Migrate database # 11. Migrate database
- source: /.cloud66/scripts/migrate.sh - source: /config/cloud/cloud66/scripts/migrate.sh
destination: /tmp/migrate.sh destination: /tmp/migrate.sh
target: rails target: rails
apply_during: build_only apply_during: build_only
execute: true execute: true
sudo: true sudo: true
# 12. Curl script # 12. Curl script
- source: /.cloud66/scripts/curl.sh - source: /config/cloud/cloud66/scripts/curl.sh
destination: /tmp/curl.sh destination: /tmp/curl.sh
target: rails target: rails
apply_during: build_only apply_during: build_only
@ -159,7 +167,7 @@ staging:
development: development:
first_thing: first_thing:
# 1. Permissions on postgres box # 1. Permissions on postgres box
- source: /.cloud66/scripts/permissions.sh - source: /config/cloud/cloud66/scripts/permissions.sh
destination: /tmp/scripts/permissions.sh destination: /tmp/scripts/permissions.sh
target: postgresql target: postgresql
apply_during: build_only apply_during: build_only
@ -174,21 +182,24 @@ development:
owner: postgres owner: postgres
after_checkout: after_checkout:
# 3. Copy Procfile # 3. Copy Procfile
- source: /.cloud66/files/Procfile - source: /config/cloud/cloud66/files/Procfile
destination: <%= ENV['RAILS_STACK_PATH'] %>/Procfile destination: <%= ENV['RAILS_STACK_PATH'] %>/Procfile
target: rails target: rails
run_on: all_servers
# 4. Rename redis.yml.sample file # 4. Rename redis.yml.sample file
- source: /.cloud66/files/redis.yml - source: /config/cloud/cloud66/files/redis.yml
destination: <%= ENV['RAILS_STACK_PATH'] %>/config/redis.yml destination: <%= ENV['RAILS_STACK_PATH'] %>/config/redis.yml
target: rails target: rails
parse: false parse: false
run_on: all_servers
# 5. Move thin config to server # 5. Move thin config to server
- source: /.cloud66/files/thin.yml - source: /config/cloud/cloud66/files/thin.yml
destination: <%= ENV['RAILS_STACK_PATH'] %>/config/thin.yml destination: <%= ENV['RAILS_STACK_PATH'] %>/config/thin.yml
target: rails target: rails
run_on: all_servers
after_rails: after_rails:
# 6. Set environment variables and allow PSQL user to access them # 6. Set environment variables and allow PSQL user to access them
- source: /.cloud66/scripts/env_vars.sh - source: /config/cloud/cloud66/scripts/env_vars.sh
destination: /tmp/scripts/env_vars.sh destination: /tmp/scripts/env_vars.sh
target: rails target: rails
apply_during: build_only apply_during: build_only
@ -196,21 +207,21 @@ development:
sudo: true sudo: true
last_thing: last_thing:
# 7. KILL DB # 7. KILL DB
- source: /.cloud66/scripts/kill_db.sh - source: /config/cloud/cloud66/scripts/kill_db.sh
destination: /tmp/scripts/kill_db.sh destination: /tmp/scripts/kill_db.sh
target: postgresql target: postgresql
apply_during: build_only apply_during: build_only
execute: true execute: true
sudo: true sudo: true
# 8. DB:DROP & DB:CREATE # 8. DB:DROP & DB:CREATE
- source: /.cloud66/scripts/drop_create.sh - source: /config/cloud/cloud66/scripts/drop_create.sh
destination: /tmp/scripts/drop_create.sh destination: /tmp/scripts/drop_create.sh
target: rails target: rails
apply_during: build_only apply_during: build_only
execute: true execute: true
sudo: true sudo: true
# 9. Import database image # 9. Import database image
- source: /.cloud66/scripts/import_dev.sh - source: /config/cloud/cloud66/scripts/import_dev.sh
destination: /tmp/scripts/import_dev.sh destination: /tmp/scripts/import_dev.sh
target: postgresql target: postgresql
apply_during: build_only apply_during: build_only
@ -218,14 +229,14 @@ development:
owner: postgres owner: postgres
run_as: postgres run_as: postgres
# 10. Migrate database # 10. Migrate database
- source: /.cloud66/scripts/migrate.sh - source: /config/cloud/cloud66/scripts/migrate.sh
destination: /tmp/migrate.sh destination: /tmp/migrate.sh
target: rails target: rails
apply_during: build_only apply_during: build_only
execute: true execute: true
sudo: true sudo: true
# 11. Curl script # 11. Curl script
- source: /.cloud66/scripts/curl.sh - source: /config/cloud/cloud66/scripts/curl.sh
destination: /tmp/curl.sh destination: /tmp/curl.sh
target: rails target: rails
apply_during: build_only apply_during: build_only

View File

@ -11,7 +11,7 @@ production:
### If you change this setting you will need to ### If you change this setting you will need to
### - restart sidekiq if you change this setting ### - restart sidekiq if you change this setting
### - rebake all to posts using: `RAILS_ENV=production bundle exec rake posts:rebake` ### - rebake all to posts using: `RAILS_ENV=production bundle exec rake posts:rebake`
- production.localhost # Update this to be the domain of your production site - <%= ENV["DISCOURSE_HOSTNAME"] || "production.localhost" %> # Update this to be the domain of your production site
test: test:
adapter: postgresql adapter: postgresql

View File

@ -975,6 +975,7 @@ en:
add_permission: "Add Permission" add_permission: "Add Permission"
this_year: "this year" this_year: "this year"
position: "position" position: "position"
parent: "Parent Category"
flagging: flagging:
title: 'Why are you flagging this post?' title: 'Why are you flagging this post?'
@ -1267,11 +1268,15 @@ en:
url: "URL" url: "URL"
screened_ips: screened_ips:
title: "Screened IPs" title: "Screened IPs"
description: "IP addresses that are being watched." description: 'IP addresses that are being watched. Use "Allow" to whitelist IP addresses.'
delete_confirm: "Are you sure you want to remove the rule for %{ip_address}?" delete_confirm: "Are you sure you want to remove the rule for %{ip_address}?"
actions: actions:
block: "Block" block: "Block"
do_nothing: "Allow" do_nothing: "Allow"
form:
label: "New:"
ip_address: "IP address"
add: "Add"
impersonate: impersonate:
title: "Impersonate User" title: "Impersonate User"

View File

@ -576,6 +576,7 @@ fr:
total: total messages total: total messages
current: message courant current: message courant
notifications: notifications:
title: ''
reasons: reasons:
'3_2': 'Vous recevrez des notifications car vous suivez attentivement cette discussion.' '3_2': 'Vous recevrez des notifications car vous suivez attentivement cette discussion.'
'3_1': 'Vous recevrez des notifications car vous avez créé cette discussion.' '3_1': 'Vous recevrez des notifications car vous avez créé cette discussion.'
@ -858,7 +859,11 @@ fr:
security: "Sécurité" security: "Sécurité"
auto_close_label: "Fermer automatiquement après :" auto_close_label: "Fermer automatiquement après :"
edit_permissions: "Editer les Permissions" edit_permissions: "Editer les Permissions"
add_permission: "AJouter une Permission" add_permission: "Ajouter une Permission"
this_year: "cette année"
position: "position"
parent: "Catégorie parente"
flagging: flagging:
title: 'Pourquoi voulez-vous signaler ce message ?' title: 'Pourquoi voulez-vous signaler ce message ?'
action: 'Signaler ce message' action: 'Signaler ce message'

View File

@ -122,6 +122,7 @@ ru:
log_in: 'Войти' log_in: 'Войти'
age: Возраст age: Возраст
last_post: 'Последнее сообщение' last_post: 'Последнее сообщение'
joined: Присоединен
admin_title: Админка admin_title: Админка
flags_title: Жалобы flags_title: Жалобы
show_more: 'показать еще' show_more: 'показать еще'
@ -202,18 +203,25 @@ ru:
sent_by_user: 'Отправлено пользователем <a href=''{{userUrl}}''>{{user}}</a>' sent_by_user: 'Отправлено пользователем <a href=''{{userUrl}}''>{{user}}</a>'
sent_by_you: 'Отправлено <a href=''{{userUrl}}''>Вами</a>' sent_by_you: 'Отправлено <a href=''{{userUrl}}''>Вами</a>'
user_action_groups: user_action_groups:
'1': 'Отдано симпатий' "1": 'Отдал симпатий'
'2': 'Получено симпатий' "2": 'Получил симпатий'
'3': Закладки "3": Закладки
'4': Темы "4": Темы
'5': Сообщения "5": Сообщения
'6': Ответы "6": Ответы
'7': Упоминания "7": Упоминания
'9': Цитаты "9": Цитаты
'10': Избранное "10": Избранное
'11': Изменения "11": Изменения
'12': 'Отправленные' "12": 'Отправленные'
'13': Входящие "13": Входящие
categories:
category: Категория
posts: Сообщения
topics: Темы
latest: Последние
latest_by: 'последние по'
toggle_ordering: 'изменить сортировку'
user: user:
said: '{{username}} писал(а):' said: '{{username}} писал(а):'
profile: Профайл profile: Профайл
@ -294,7 +302,7 @@ ru:
title: 'Пароль еще раз' title: 'Пароль еще раз'
last_posted: 'Последнее сообщение' last_posted: 'Последнее сообщение'
last_emailed: 'Последнее письмо' last_emailed: 'Последнее письмо'
last_seen: 'Посл. визит' last_seen: 'Последний визит'
created: 'Регистрация' created: 'Регистрация'
log_out: 'Выйти' log_out: 'Выйти'
website: 'Веб-сайт' website: 'Веб-сайт'
@ -487,8 +495,8 @@ ru:
link_optional_text: 'необязательное название' link_optional_text: 'необязательное название'
quote_title: Цитата quote_title: Цитата
quote_text: Цитата quote_text: Цитата
code_title: рагмент кода' code_title: орматированный текст'
code_text: 'вводите код здесь' code_text: 'введите форматированный текст'
upload_title: Загрузить upload_title: Загрузить
upload_description: 'введите здесь описание загружаемого объекта' upload_description: 'введите здесь описание загружаемого объекта'
olist_title: 'Нумерованный список' olist_title: 'Нумерованный список'
@ -528,6 +536,8 @@ ru:
remote_tip_with_attachments: 'Введите адрес изображения или файла в формате http://example.com/file.ext (список доступных расширений: {{authorized_extensions}}).' remote_tip_with_attachments: 'Введите адрес изображения или файла в формате http://example.com/file.ext (список доступных расширений: {{authorized_extensions}}).'
local_tip: 'кликните для выбора изображения с вашего устройства' local_tip: 'кликните для выбора изображения с вашего устройства'
local_tip_with_attachments: 'кликните для выбора изображения с вашего устройства (доступные расширения: {{authorized_extensions}})' local_tip_with_attachments: 'кликните для выбора изображения с вашего устройства (доступные расширения: {{authorized_extensions}})'
hint: '(вы так же можете перетащить объект в редактор для его загрузки)'
hint_for_chrome: '(вы так же можете перетащить или вставить изображение в редактор для его загрузки)'
uploading: Загрузка uploading: Загрузка
search: search:
title: 'поиск по темам, сообщениям, пользователям или категориям' title: 'поиск по темам, сообщениям, пользователям или категориям'
@ -576,6 +586,16 @@ ru:
private_message: 'Написать личное сообщение' private_message: 'Написать личное сообщение'
list: Темы list: Темы
new: 'новая тема' new: 'новая тема'
new_topics:
one: '1 новая тема'
other: '{{count}} новых тем'
few: '{{count}} новых темы'
many: '{{count}} новых тем'
unread_topics:
one: '1 непрочитанная тема'
other: '{{count}} непрочитанных тем'
few: '{{count}} непрочитанных темы'
many: '{{count}} непрочитанных тем'
title: Тема title: Тема
loading_more: 'Загружаю темы...' loading_more: 'Загружаю темы...'
loading: 'Загружаю тему...' loading: 'Загружаю тему...'
@ -630,16 +650,16 @@ ru:
notifications: notifications:
title: '&nbsp;' title: '&nbsp;'
reasons: reasons:
'3_2': 'Вы будете получать уведомления, потому что вы наблюдаете за темой.' "3_2": 'Вы будете получать уведомления, потому что вы наблюдаете за темой.'
'3_1': 'Вы будете получать уведомления, потому что вы создали тему.' "3_1": 'Вы будете получать уведомления, потому что вы создали тему.'
'3': 'Вы будете получать уведомления, потому что вы наблюдаете за темой.' "3": 'Вы будете получать уведомления, потому что вы наблюдаете за темой.'
'2_4': 'Вы будете получать уведомления, потому что вы ответили в теме.' "2_4": 'Вы будете получать уведомления, потому что вы ответили в теме.'
'2_2': 'Вы будете получать уведомления, потому что вы отслеживаете тему.' "2_2": 'Вы будете получать уведомления, потому что вы отслеживаете тему.'
'2': 'Вы будете получать уведомления, потому что вы <a href="/users/{{username}}/preferences">читали тему</a>.' "2": 'Вы будете получать уведомления, потому что вы <a href="/users/{{username}}/preferences">читали тему</a>.'
'1': 'Вы получите уведомление, только если кто-нибудь упомянет вас по @name или ответит на ваше сообщение.' "1": 'Вы получите уведомление, только если кто-нибудь упомянет вас по @name или ответит на ваше сообщение.'
'1_2': 'Вы получите уведомление, только если кто-нибудь упомянет вас по @name или ответит на ваше сообщение.' "1_2": 'Вы получите уведомление, только если кто-нибудь упомянет вас по @name или ответит на ваше сообщение.'
'0': 'Вы не получаете уведомления по теме.' "0": 'Вы не получаете уведомления по теме.'
'0_2': 'Вы не получаете уведомления по теме.' "0_2": 'Вы не получаете уведомления по теме.'
watching: watching:
title: Наблюдение title: Наблюдение
description: 'то же самое, что и режим отслеживания, но вы дополнительно будете получать уведомления обо всех новых сообщениях.' description: 'то же самое, что и режим отслеживания, но вы дополнительно будете получать уведомления обо всех новых сообщениях.'
@ -939,7 +959,7 @@ ru:
category: category:
can: 'может&hellip; ' can: 'может&hellip; '
none: '(без категории)' none: '(без категории)'
choose: 'Select a category&hellip;' choose: 'Выберете категорию&hellip;'
edit: изменить edit: изменить
edit_long: 'Изменить категорию' edit_long: 'Изменить категорию'
view: 'Просмотр тем по категориям' view: 'Просмотр тем по категориям'
@ -970,13 +990,16 @@ ru:
auto_close_label: 'Закрыть тему через:' auto_close_label: 'Закрыть тему через:'
edit_permissions: 'Изменить права доступа' edit_permissions: 'Изменить права доступа'
add_permission: 'Добавить права' add_permission: 'Добавить права'
this_year: 'в год'
position: местоположение
parent: 'Родительская категория'
flagging: flagging:
title: 'Выберите действие над сообщением' title: 'Выберите действие над сообщением'
action: 'Пожаловаться' action: 'Пожаловаться'
take_action: 'Принять меры' take_action: 'Принять меры'
notify_action: Отправить notify_action: Отправить
delete_spammer: 'Удалить спамера' delete_spammer: 'Удалить спамера'
delete_confirm: 'Вы собираетесь удалить <b>%{posts}</b> сообщений и <b>%{topics}</b> тем этого пользователя, а так же удалить его учетную запись и добавить его почтовый адрес <b>%{email}</b> в черный список. Вы действительно уверены, что ваши помыслы чисты и действия не продиктованы гневом?' delete_confirm: 'Вы собираетесь удалить <b>%{posts}</b> сообщений и <b>%{topics}</b> тем этого пользователя, а так же удалить его учетную запись, добавить его IP адрес <b>%{ip_address}</b> и его почтовый адрес <b>%{email}</b> в черный список. Вы действительно уверены, что ваши помыслы чисты и действия не продиктованы гневом?'
yes_delete_spammer: 'Да, удалить спамера' yes_delete_spammer: 'Да, удалить спамера'
cant: 'Извините, но вы не можете сейчас послать жалобу.' cant: 'Извините, но вы не можете сейчас послать жалобу.'
custom_placeholder_notify_user: 'Почему это сообщение побудило вас обратиться к этому пользователю напрямую и в частном порядке? Будьте конкретны, будьте конструктивны и всегда доброжелательны.' custom_placeholder_notify_user: 'Почему это сообщение побудило вас обратиться к этому пользователю напрямую и в частном порядке? Будьте конкретны, будьте конструктивны и всегда доброжелательны.'
@ -1159,12 +1182,18 @@ ru:
delete_confirm: 'Удалить данную группу?' delete_confirm: 'Удалить данную группу?'
delete_failed: 'Невозможно удалить группу. Если это автоматически созданная группа, то она не может быть удалена.' delete_failed: 'Невозможно удалить группу. Если это автоматически созданная группа, то она не может быть удалена.'
api: api:
generate_master: 'Сгенерировать ключ API'
none: 'Отсутствует ключ API.'
user: Пользователь
title: API title: API
long_title: 'Информация об API' key: 'Ключ API'
key: Ключ generate: Сгенерировать
generate: 'Сгенерировать ключ API' regenerate: Перегенерировать
regenerate: 'Сгенерировать ключ API заново' revoke: Отозвать
confirm_regen: 'Вы уверены, что хотите заменить ключ API?'
confirm_revoke: 'Вы уверены, что хотите отозвать этот ключ?'
info_html: 'Ваш API ключ позволит вам создавать и обновлять темы, используя JSON calls.' info_html: 'Ваш API ключ позволит вам создавать и обновлять темы, используя JSON calls.'
all_users: 'Все пользователи'
note_html: 'Никому <strong>не сообщайте</strong> эти ключи, Тот, у кого они есть, сможет создавать сообщения, выдавая себя за любого пользователя форума.' note_html: 'Никому <strong>не сообщайте</strong> эти ключи, Тот, у кого они есть, сможет создавать сообщения, выдавая себя за любого пользователя форума.'
customize: customize:
title: Оформление title: Оформление
@ -1210,6 +1239,9 @@ ru:
last_match_at: 'Последнее совпадение' last_match_at: 'Последнее совпадение'
match_count: Совпадения match_count: Совпадения
ip_address: IP ip_address: IP
delete: Удалить
edit: Изменить
save: Сохранить
screened_actions: screened_actions:
block: блокировать block: блокировать
do_nothing: 'ничего не делать' do_nothing: 'ничего не делать'
@ -1244,6 +1276,17 @@ ru:
title: 'Отслеживаемые ссылки' title: 'Отслеживаемые ссылки'
description: 'Список ссылок от пользователей, которые были идентифицированы как спамеры.' description: 'Список ссылок от пользователей, которые были идентифицированы как спамеры.'
url: URL url: URL
screened_ips:
title: 'Наблюдаемые IP адреса'
description: 'IP адреса за которыми вести наблюдение. Используйте "Разрешить" для добавления IP адреса в белый список.'
delete_confirm: 'Вы уверены, что хотите удалить правило для %{ip_address}?'
actions:
block: Заблокировать
do_nothing: Разрешить
form:
label: 'Новые:'
ip_address: 'IP адрес'
add: Добавить
impersonate: impersonate:
title: 'Представиться как пользователь' title: 'Представиться как пользователь'
username_or_email: 'Имя пользователя или Email' username_or_email: 'Имя пользователя или Email'
@ -1325,7 +1368,7 @@ ru:
reputation: Репутация reputation: Репутация
permissions: Права permissions: Права
activity: Активность activity: Активность
like_count: 'Получено симпатий' like_count: 'Получил симпатий'
private_topics_count: 'Частные темы' private_topics_count: 'Частные темы'
posts_read_count: 'Прочитано сообщений' posts_read_count: 'Прочитано сообщений'
post_count: 'Создано сообщений' post_count: 'Создано сообщений'
@ -1344,8 +1387,8 @@ ru:
few: 'Пользователь не может быть удален, если он зарегистрирован больше чем %{count} дня назад и у него есть сообщения. Удалите все сообщения перед удалением пользователя.' few: 'Пользователь не может быть удален, если он зарегистрирован больше чем %{count} дня назад и у него есть сообщения. Удалите все сообщения перед удалением пользователя.'
many: 'Пользователь не может быть удален, если он зарегистрирован больше чем %{count} дней назад и у него есть сообщения. Удалите все сообщения перед удалением пользователя.' many: 'Пользователь не может быть удален, если он зарегистрирован больше чем %{count} дней назад и у него есть сообщения. Удалите все сообщения перед удалением пользователя.'
delete_confirm: 'Вы уверены, что хотите удалить пользователя? Это действие необратимо!' delete_confirm: 'Вы уверены, что хотите удалить пользователя? Это действие необратимо!'
delete_and_block: '<b>Да</b>, и <b>заблокировать</b> повторную регистрацию с данного адреса' delete_and_block: '<b>Да</b>, и <b>запретить</b> регистрацию с данного email и IP адреса'
delete_dont_block: '<b>Да</b>, но <b>разрешить</b> повторную регистрацию с данного адреса' delete_dont_block: '<b>Да</b>, но <b>разрешить</b> регистрацию с данного email и IP адреса'
deleted: 'Пользователь удален.' deleted: 'Пользователь удален.'
delete_failed: 'При удалении пользователя возникла ошибка. Для удаления пользователя необходимо сначала удалить все его сообщения.' delete_failed: 'При удалении пользователя возникла ошибка. Для удаления пользователя необходимо сначала удалить все его сообщения.'
send_activation_email: 'Послать активационное письмо' send_activation_email: 'Послать активационное письмо'

View File

@ -975,6 +975,7 @@ zh_CN:
add_permission: "添加权限" add_permission: "添加权限"
this_year: "今年" this_year: "今年"
position: "位置" position: "位置"
parent: "上级分类"
flagging: flagging:
title: '为何要报告本帖?' title: '为何要报告本帖?'
@ -1267,11 +1268,15 @@ zh_CN:
url: "URL" url: "URL"
screened_ips: screened_ips:
title: "被屏蔽的IP" title: "被屏蔽的IP"
description: "受监视的IP地址。" description: "受监视的IP地址使用“放行”可将IP地址加入白名单。"
delete_confirm: "确定要撤销对IP地址为 %{ip_address} 的规则?" delete_confirm: "确定要撤销对IP地址为 %{ip_address} 的规则?"
actions: actions:
block: "阻挡" block: "阻挡"
do_nothing: "放行" do_nothing: "放行"
form:
label: "新:"
ip_address: "IP地址"
add: "添加"
impersonate: impersonate:
title: "假冒用户" title: "假冒用户"

View File

@ -159,7 +159,9 @@ en:
topic_prefix: "Category definition for %{category}" topic_prefix: "Category definition for %{category}"
replace_paragraph: "[Replace this first paragraph with a short description of your new category. This guidance will appear in the category selection area, so try to keep it below 200 characters.]" replace_paragraph: "[Replace this first paragraph with a short description of your new category. This guidance will appear in the category selection area, so try to keep it below 200 characters.]"
post_template: "%{replace_paragraph}\n\nUse the following paragraphs for a longer description, as well as to establish any category guidelines or rules.\n\nSome things to consider in any discussion replies below:\n\n- What is this category for? Why should people select this category for their topic?\n\n- How is this different than the other categories we already have?\n\n- Do we need this category?\n\n- Should we merge this with another category, or split it into more categories?\n" post_template: "%{replace_paragraph}\n\nUse the following paragraphs for a longer description, as well as to establish any category guidelines or rules.\n\nSome things to consider in any discussion replies below:\n\n- What is this category for? Why should people select this category for their topic?\n\n- How is this different than the other categories we already have?\n\n- Do we need this category?\n\n- Should we merge this with another category, or split it into more categories?\n"
errors:
self_parent: "A subcategory's parent cannot be itself."
depth: "You can't nest a subcategory under another."
trust_levels: trust_levels:
newuser: newuser:
title: "new user" title: "new user"
@ -416,6 +418,7 @@ en:
dashboard: dashboard:
rails_env_warning: "Your server is running in %{env} mode." rails_env_warning: "Your server is running in %{env} mode."
ruby_version_warning: "You are running a version of Ruby 2.0.0 that is known to have problems. Upgrade to patch level 247 or later."
host_names_warning: "Your config/database.yml file is using the default localhost hostname. Update it to use your site's hostname." host_names_warning: "Your config/database.yml file is using the default localhost hostname. Update it to use your site's hostname."
gc_warning: 'Your server is using default ruby garbage collection parameters, which will not give you the best performance. Read this topic on performance tuning: <a href="http://meta.discourse.org/t/tuning-ruby-and-rails-for-discourse/4126" target="_blank">Tuning Ruby and Rails for Discourse</a>.' gc_warning: 'Your server is using default ruby garbage collection parameters, which will not give you the best performance. Read this topic on performance tuning: <a href="http://meta.discourse.org/t/tuning-ruby-and-rails-for-discourse/4126" target="_blank">Tuning Ruby and Rails for Discourse</a>.'
sidekiq_warning: 'Sidekiq is not running. Many tasks, like sending emails, are executed asynchronously by sidekiq. Please ensure at least one sidekiq process is running. <a href="https://github.com/mperham/sidekiq" target="_blank">Learn about Sidekiq here</a>.' sidekiq_warning: 'Sidekiq is not running. Many tasks, like sending emails, are executed asynchronously by sidekiq. Please ensure at least one sidekiq process is running. <a href="https://github.com/mperham/sidekiq" target="_blank">Learn about Sidekiq here</a>.'

View File

@ -699,7 +699,7 @@ fr:
Si cela vous intéresse, cliquez sur le lien ci-dessous pour suivre la discussion : Si cela vous intéresse, cliquez sur le lien ci-dessous pour suivre la discussion :
[Visit %{site_name}][1] [Visiter %{site_name}][1]
Vous avez été invité(e) par un utilisateur de confiance, donc vous avez la possibilité de répondre sans même avoir besoin de vous connecter. Vous avez été invité(e) par un utilisateur de confiance, donc vous avez la possibilité de répondre sans même avoir besoin de vous connecter.

View File

@ -100,6 +100,8 @@ ru:
name: 'Название категории' name: 'Название категории'
post: post:
raw: Текст сообщения raw: Текст сообщения
user:
ip_address: ''
errors: errors:
messages: messages:
is_invalid: 'слишком короткий' is_invalid: 'слишком короткий'
@ -109,6 +111,10 @@ ru:
attributes: attributes:
archetype: archetype:
cant_send_pm: 'Извините, вы не можете посылать личные сообщения данному пользователю.' cant_send_pm: 'Извините, вы не можете посылать личные сообщения данному пользователю.'
user:
attributes:
ip_address:
signup_not_allowed: 'Регистрация с данной учетной записью запрещена.'
user_profile: user_profile:
no_info_me: '<div class=''missing-profile''>Поле Обо мне в вашем профиле не заполнено, <a href=''/users/%{username_lower}/preferences/about-me''>не желаете ли что-нибудь написать в нем?</a></div>' no_info_me: '<div class=''missing-profile''>Поле Обо мне в вашем профиле не заполнено, <a href=''/users/%{username_lower}/preferences/about-me''>не желаете ли что-нибудь написать в нем?</a></div>'
no_info_other: '<div class=''missing-profile''>%{name} еще не заполнил поле «Обо мне» в своём профайле. </div>' no_info_other: '<div class=''missing-profile''>%{name} еще не заполнил поле «Обо мне» в своём профайле. </div>'
@ -116,6 +122,9 @@ ru:
topic_prefix: 'Описание категории %{category}' topic_prefix: 'Описание категории %{category}'
replace_paragraph: '[Замените данный текст кратким описанием новой категории. Это описание будет отображаться в списке категорий, поэтому постарайтесь сделать его коротким (не более 200 символов).]' replace_paragraph: '[Замените данный текст кратким описанием новой категории. Это описание будет отображаться в списке категорий, поэтому постарайтесь сделать его коротким (не более 200 символов).]'
post_template: "%{replace_paragraph}\n\nВ данное поле введите более подробное описание категории, а также возможные правила опубликования в ней тем.\n\nНесколько аспектов, которые следует учитывать:\n\n- Для чего нужна данная категория? Почему люди выберут данную категорию для размещения своей темы?\n\n- Чем данная категория отличается от тех, которые у нас уже есть?\n\n- Нужна ли нам эта категория?\n\n- Стоит ли нам объединить ее с другой категорией или разбить на несколько?\n" post_template: "%{replace_paragraph}\n\nВ данное поле введите более подробное описание категории, а также возможные правила опубликования в ней тем.\n\nНесколько аспектов, которые следует учитывать:\n\n- Для чего нужна данная категория? Почему люди выберут данную категорию для размещения своей темы?\n\n- Чем данная категория отличается от тех, которые у нас уже есть?\n\n- Нужна ли нам эта категория?\n\n- Стоит ли нам объединить ее с другой категорией или разбить на несколько?\n"
errors:
self_parent: 'Подкатегория не может быть родительской для самой себя.'
depth: 'Вы не можете иметь вложенные подкатегории.'
trust_levels: trust_levels:
newuser: newuser:
title: 'новый пользователь' title: 'новый пользователь'
@ -408,6 +417,7 @@ ru:
num_clicks: Переходов num_clicks: Переходов
dashboard: dashboard:
rails_env_warning: 'Ваш сервер работает в режиме %{env}.' rails_env_warning: 'Ваш сервер работает в режиме %{env}.'
ruby_version_warning: 'Вы используете версию Ruby 2.0.0, у которой имеются известные проблемы. Обновите версию до patch-247 или более новую.'
host_names_warning: 'Ваш файл config/database.yml использует локальное имя хоста по умолчанию. Поменяйте его на имя хоста вашего сайта.' host_names_warning: 'Ваш файл config/database.yml использует локальное имя хоста по умолчанию. Поменяйте его на имя хоста вашего сайта.'
gc_warning: 'Ваш сервер использует параметры ruby garbage collection по умолчанию, что ведет не к лучшей производительности. Прочитайте эту тему про настройку производительности: <a href="http://meta.discourse.org/t/tuning-ruby-and-rails-for-discourse/4126">Tuning Ruby and Rails for Discourse</a>.' gc_warning: 'Ваш сервер использует параметры ruby garbage collection по умолчанию, что ведет не к лучшей производительности. Прочитайте эту тему про настройку производительности: <a href="http://meta.discourse.org/t/tuning-ruby-and-rails-for-discourse/4126">Tuning Ruby and Rails for Discourse</a>.'
sidekiq_warning: 'Sidekiq не запущен. Сейчас многие задачи, такие как отправка электронных писем, выполняются асинхронно. Пожалуйста, убедитесь, что хотя бы один процесс sidekiq запущен. <a href="https://github.com/mperham/sidekiq">Узнайте больше о Sidekiq здесь</a>.' sidekiq_warning: 'Sidekiq не запущен. Сейчас многие задачи, такие как отправка электронных писем, выполняются асинхронно. Пожалуйста, убедитесь, что хотя бы один процесс sidekiq запущен. <a href="https://github.com/mperham/sidekiq">Узнайте больше о Sidekiq здесь</a>.'
@ -518,9 +528,12 @@ ru:
new_topic_duration_minutes: 'Глобальное количество минут по умолчанию, в течение которых тема оценивается как новая, пользователи могут переназначать (-1 - всегда, -2 для последнего посещения)' new_topic_duration_minutes: 'Глобальное количество минут по умолчанию, в течение которых тема оценивается как новая, пользователи могут переназначать (-1 - всегда, -2 для последнего посещения)'
flags_required_to_hide_post: 'Количество жалоб, по достижении которого сообщение автоматически скрывается, и личное сообщение об этом посылается автору (0 - никогда)' flags_required_to_hide_post: 'Количество жалоб, по достижении которого сообщение автоматически скрывается, и личное сообщение об этом посылается автору (0 - никогда)'
cooldown_minutes_after_hiding_posts: 'Количество минут, которое должен подождать пользователь перед редактированием сообщения скрытого по жалобам' cooldown_minutes_after_hiding_posts: 'Количество минут, которое должен подождать пользователь перед редактированием сообщения скрытого по жалобам'
max_topics_in_first_day: 'Максимальное количество тем, которое пользователь может создать в первый день на сайте'
max_replies_in_first_day: 'Максимальное количество ответов, которое пользователь может сделать в первый день на сайте'
num_flags_to_block_new_user: 'Если сообщения нового пользователя получат данное количество флагов от различных пользователей, скрыть все сообщения пользователя и отказать пользователю в публикации новых сообщений. 0 отключает функцию.' num_flags_to_block_new_user: 'Если сообщения нового пользователя получат данное количество флагов от различных пользователей, скрыть все сообщения пользователя и отказать пользователю в публикации новых сообщений. 0 отключает функцию.'
num_users_to_block_new_user: 'Если сообщения нового пользователя получат флаги от данного количества различных пользователей, скрыть все сообщения пользователя и отказать пользователю в публикации новых сообщений. 0 отключает функцию.' num_users_to_block_new_user: 'Если сообщения нового пользователя получат флаги от данного количества различных пользователей, скрыть все сообщения пользователя и отказать пользователю в публикации новых сообщений. 0 отключает функцию.'
notify_mods_when_user_blocked: 'Отправить сообщение всем модераторам, если пользователь заблокирован автоматически.' notify_mods_when_user_blocked: 'Отправить сообщение всем модераторам, если пользователь заблокирован автоматически.'
flag_sockpuppets: 'Если новый пользователь (например, зарегистрированный за последние 24 часа), который начал тему и новый пользователь, который ответил в теме, имеют одинаковые IP адреса, помечать оба сообщения как спам.'
traditional_markdown_linebreaks: 'Использовать стандартные разрывы строк в Markdown, вместо двух пробелов' traditional_markdown_linebreaks: 'Использовать стандартные разрывы строк в Markdown, вместо двух пробелов'
post_undo_action_window_mins: 'Количество секунд, в течение которых пользователь может отменить действие («Мне нравится», «Жалоба» и т.д.)' post_undo_action_window_mins: 'Количество секунд, в течение которых пользователь может отменить действие («Мне нравится», «Жалоба» и т.д.)'
must_approve_users: 'Администраторы должны одобрять учетные записи всех новых пользователей для того, чтобы они получили доступ' must_approve_users: 'Администраторы должны одобрять учетные записи всех новых пользователей для того, чтобы они получили доступ'
@ -578,6 +591,8 @@ ru:
max_topics_per_day: 'Максимальное количество тем, которое пользователь может создать в день' max_topics_per_day: 'Максимальное количество тем, которое пользователь может создать в день'
max_private_messages_per_day: 'Максимальное количество личных сообщений, которое пользователь может послать в день' max_private_messages_per_day: 'Максимальное количество личных сообщений, которое пользователь может послать в день'
suggested_topics: 'Количество рекомендованных тем, отображаемых внизу текущей темы' suggested_topics: 'Количество рекомендованных тем, отображаемых внизу текущей темы'
clean_up_uploads: 'Удалить неиспользуемые загрузки для предотвращения хранения нелегального контента. ВНИМАНИЕ: рекомендуется сделать резервную копию директории /uploads перед включением данной настройки.'
uploads_grace_period_in_hours: 'Период (в часах) после которого неопубликованные вложения удаляются.'
enable_s3_uploads: 'Размещать загруженные файлы на Amazon S3' enable_s3_uploads: 'Размещать загруженные файлы на Amazon S3'
s3_upload_bucket: 'Наименование Amazon S3 bucket в который будут загружаться файлы. ВНИМАНИЕ: имя должно быть в нижнем регистре (см. http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html)' s3_upload_bucket: 'Наименование Amazon S3 bucket в который будут загружаться файлы. ВНИМАНИЕ: имя должно быть в нижнем регистре (см. http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html)'
s3_access_key_id: 'Amazon S3 access key для загрузки и хранения изображений' s3_access_key_id: 'Amazon S3 access key для загрузки и хранения изображений'
@ -613,6 +628,7 @@ ru:
min_title_similar_length: 'Минимальная длина названия темы, при которой тема будет проверена на наличие похожих' min_title_similar_length: 'Минимальная длина названия темы, при которой тема будет проверена на наличие похожих'
min_body_similar_length: 'Минимальная длина тела сообщения, при которой оно будет проверено на наличие похожих тем' min_body_similar_length: 'Минимальная длина тела сообщения, при которой оно будет проверено на наличие похожих тем'
category_colors: 'Разделенный чертой (|) список дозволенных hexadecimal цветов для категорий' category_colors: 'Разделенный чертой (|) список дозволенных hexadecimal цветов для категорий'
enable_wide_category_list: 'Включить традиционный полноразмерный список категорий.'
max_image_size_kb: 'Максимальный размер изображений для загрузки пользователем в КБ убедитесь, что вы так же настроили лимит в nginx (client_max_body_size) / apache или прокси.' max_image_size_kb: 'Максимальный размер изображений для загрузки пользователем в КБ убедитесь, что вы так же настроили лимит в nginx (client_max_body_size) / apache или прокси.'
max_attachment_size_kb: 'Максимальный размер файлов для загрузки пользователем в кб убедитесь, что вы настроили лимит также в nginx (client_max_body_size) / apache или proxy.' max_attachment_size_kb: 'Максимальный размер файлов для загрузки пользователем в кб убедитесь, что вы настроили лимит также в nginx (client_max_body_size) / apache или proxy.'
authorized_extensions: 'Список расширений файлов, разрешенных к загрузке, разделенный вертикальной чертой (|)' authorized_extensions: 'Список расширений файлов, разрешенных к загрузке, разделенный вертикальной чертой (|)'
@ -718,6 +734,8 @@ ru:
email: email:
not_allowed: 'недопустимый почтовый домен. Пожалуйста, используйте другой адрес.' not_allowed: 'недопустимый почтовый домен. Пожалуйста, используйте другой адрес.'
blocked: 'не разрешено.' blocked: 'не разрешено.'
ip_address:
blocked: 'блокирован.'
invite_mailer: invite_mailer:
subject_template: '[%{site_name}] %{invitee_name} пригласил вас присоединиться к обсуждению на сайте %{site_name}' subject_template: '[%{site_name}] %{invitee_name} пригласил вас присоединиться к обсуждению на сайте %{site_name}'
text_body_template: "%{invitee_name} пригласил вас в тему \"%{topic_title}\" на сайте %{site_name}.\n\nЕсли вам интересно, пройдите по ссылке ниже, чтобы попасть в обсуждение:\n\n[%{site_name}][1]\n\nВы приглашены доверенным пользователем, поэтому сразу сможете разместить свой ответ без входа на сайт.\n\n[1]: %{invite_link}\n" text_body_template: "%{invitee_name} пригласил вас в тему \"%{topic_title}\" на сайте %{site_name}.\n\nЕсли вам интересно, пройдите по ссылке ниже, чтобы попасть в обсуждение:\n\n[%{site_name}][1]\n\nВы приглашены доверенным пользователем, поэтому сразу сможете разместить свой ответ без входа на сайт.\n\n[1]: %{invite_link}\n"
@ -756,8 +774,8 @@ ru:
subject_template: 'Новый пользователь %{username} заблокирован по жалобам' subject_template: 'Новый пользователь %{username} заблокирован по жалобам'
text_body_template: "Это автоматическое сообщение для информирования вас о том, что новый пользователь [%{username}](%{base_url}%{user_url}) был автоматически заблокирован из за жалоб других пользователей на сообщения %{username}.\n\nПожалуйста [проверьте жалобы](/admin/flags). Если пользователь %{username} был заблокирован неверно, нажмите кнопку разблокировки [на странице управления пользователем](%{base_url}%{user_url}).\n\nПорог блокировки может быть изменен в настройке сайта `block_new_user`.\n" text_body_template: "Это автоматическое сообщение для информирования вас о том, что новый пользователь [%{username}](%{base_url}%{user_url}) был автоматически заблокирован из за жалоб других пользователей на сообщения %{username}.\n\nПожалуйста [проверьте жалобы](/admin/flags). Если пользователь %{username} был заблокирован неверно, нажмите кнопку разблокировки [на странице управления пользователем](%{base_url}%{user_url}).\n\nПорог блокировки может быть изменен в настройке сайта `block_new_user`.\n"
spam_post_blocked: spam_post_blocked:
subject_template: 'Новый пользователь %{username} блокирован за повторяющиеся ссылки в сообщениях' subject_template: 'Сообщения нового пользователя %{username} блокированы за повторяющиеся ссылки'
text_body_template: "Это автоматическое сообщение для информирования вас о том, что новый пользователь [%{username}](%{base_url}%{user_url}) пытался создать множество сообщений с ссылками на %{domains} и был заблокирован для предотвращения спам-рассылки.\n\nПожалуйста [проверьте действия пользователя](%{base_url}%{user_url}).\n\nПороговое значение может быть изменено в настройке сайта `newuser_spam_host_threshold`.\n" text_body_template: "Это автоматическое сообщение для информирования вас о том, что новый пользователь [%{username}](%{base_url}%{user_url}) пытался создать множество сообщений с ссылками на %{domains}, однако они были заблокированы для предотвращения спама. Пользователь по прежнему может создавать новые сообщения, которые не ссылаются на %{domains}.\n\nПожалуйста [проверьте действия пользователя](%{base_url}%{user_url}).\n\nПороговое значение может быть изменено в настройке сайта `newuser_spam_host_threshold`.\n"
unblocked: unblocked:
subject_template: 'Учетная запись разблокирована' subject_template: 'Учетная запись разблокирована'
text_body_template: "Здравствуйте!\n\nЭто автоматическое сообщение сайта %{site_name}. Ваш аккаунт был разблокирован. Теперь вы можете создавать новые темы и отвечать в них.\n" text_body_template: "Здравствуйте!\n\nЭто автоматическое сообщение сайта %{site_name}. Ваш аккаунт был разблокирован. Теперь вы можете создавать новые темы и отвечать в них.\n"
@ -839,3 +857,6 @@ ru:
fetch_failure: 'Извините, во время извлечения изображения произошла ошибка.' fetch_failure: 'Извините, во время извлечения изображения произошла ошибка.'
unknown_image_type: 'Файл, который вы загружаете, не является изображением.' unknown_image_type: 'Файл, который вы загружаете, не является изображением.'
size_not_found: 'Извините, мы не можем определить размер изображения. Возможно, изображение повреждено?' size_not_found: 'Извините, мы не можем определить размер изображения. Возможно, изображение повреждено?'
flag_reason:
sockpuppet: 'Новый пользователь создал тему и другой новый пользователь с того же IP адреса ответил. Для получения подробностей см. настройку сайта flag_sockpuppets.'
spam_hosts: 'Пользователь пытался создать множество сообщений с ссылками на один и тот же домен. Для получения подробностей см. настройку сайта newuser_spam_host_threshold.'

View File

@ -2,6 +2,9 @@
"app/assets/javascripts/discourse/components/*.js": { "app/assets/javascripts/discourse/components/*.js": {
"command": "dcomponent" "command": "dcomponent"
}, },
"app/assets/javascripts/discourse/lib/*.js": {
"command": "dlib"
},
"app/assets/javascripts/discourse/routes/*.js": { "app/assets/javascripts/discourse/routes/*.js": {
"command": "droute" "command": "droute"
}, },

View File

@ -69,7 +69,7 @@ Discourse::Application.routes.draw do
scope '/logs' do scope '/logs' do
resources :staff_action_logs, only: [:index] resources :staff_action_logs, only: [:index]
resources :screened_emails, only: [:index] resources :screened_emails, only: [:index]
resources :screened_ip_addresses, only: [:index, :update, :destroy] resources :screened_ip_addresses, only: [:index, :create, :update, :destroy]
resources :screened_urls, only: [:index] resources :screened_urls, only: [:index]
end end
@ -210,8 +210,10 @@ Discourse::Application.routes.draw do
get "#{filter}" => "list##{filter}" get "#{filter}" => "list##{filter}"
get "#{filter}/more" => "list##{filter}" get "#{filter}/more" => "list##{filter}"
get "category/:category/#{filter}" => "list##{filter}" get "category/:category/l/#{filter}" => "list##{filter}"
get "category/:category/#{filter}/more" => "list##{filter}" get "category/:category/l/#{filter}/more" => "list##{filter}"
get "category/:parent_category/:category/l/#{filter}" => "list##{filter}"
get "category/:parent_category/:category/l/#{filter}/more" => "list##{filter}"
end end
get 'category/:parent_category/:category' => 'list#category', as: 'category_list_parent' get 'category/:parent_category/:category' => 'list#category', as: 'category_list_parent'

20
db/fixtures/categories.rb Normal file
View File

@ -0,0 +1,20 @@
SiteSetting.refresh!
if SiteSetting.uncategorized_category_id == -1
puts "Seeding uncategorized category!"
result = Category.exec_sql "SELECT 1 FROM categories WHERE name = 'uncategorized'"
name = 'uncategorized'
if result.count > 0
name << SecureRandom.hex
end
result = Category.exec_sql "INSERT INTO categories
(name,color,slug,description,text_color, user_id, created_at, updated_at, position)
VALUES ('#{name}', 'AB9364', 'uncategorized', '', 'FFFFFF', -1, now(), now(), 1 )
RETURNING id
"
category_id = result[0]["id"].to_i
Category.exec_sql "INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
VALUES ('uncategorized_category_id', 3, #{category_id}, now(), now())"
end

Some files were not shown because too many files have changed in this diff Show More