mirror of
https://github.com/discourse/discourse.git
synced 2025-02-17 16:55:08 +00:00
catching up with the master
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
cafc1a088d
@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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')}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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}}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
@ -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);
|
||||||
|
@ -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()
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
60
app/assets/javascripts/discourse/lib/html.js
Normal file
60
app/assets/javascripts/discourse/lib/html.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
@ -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));
|
@ -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'
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
@ -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'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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" });
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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}}
|
||||||
|
|
||||||
|
@ -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}}
|
@ -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>
|
@ -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}}
|
||||||
|
@ -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>
|
||||||
|
@ -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}}
|
||||||
|
@ -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>
|
||||||
|
@ -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}}
|
||||||
|
@ -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>
|
||||||
|
@ -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}}
|
@ -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}}
|
||||||
|
@ -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> {{i18n admin.user.show_admin_profile}}</a>
|
<a {{bindAttr href="adminPath"}} class='btn right'><i class="icon-wrench"></i> {{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>
|
||||||
|
@ -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() {
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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.$();
|
||||||
|
@ -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();
|
||||||
|
@ -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>";
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Notification badge
|
// Notification badge
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
45
app/services/spam_rule/auto_block.rb
Normal file
45
app/services/spam_rule/auto_block.rb
Normal 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
|
37
app/services/spam_rule/flag_sockpuppets.rb
Normal file
37
app/services/spam_rule/flag_sockpuppets.rb
Normal 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
|
@ -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
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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'
|
||||||
|
@ -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: ' '
|
title: ' '
|
||||||
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: 'может… '
|
can: 'может… '
|
||||||
none: '(без категории)'
|
none: '(без категории)'
|
||||||
choose: 'Select a category…'
|
choose: 'Выберете категорию…'
|
||||||
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: 'Послать активационное письмо'
|
||||||
|
@ -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: "假冒用户"
|
||||||
|
@ -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>.'
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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.'
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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
20
db/fixtures/categories.rb
Normal 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
Loading…
x
Reference in New Issue
Block a user