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('loading', false);
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
recordAdded: function(arg) {
|
||||
this.get("content").unshiftObject(arg);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -27,12 +33,12 @@ Discourse.AdminLogsScreenedIpAddressController = Ember.ObjectController.extend({
|
|||
|
||||
actions: {
|
||||
allow: function(record) {
|
||||
record.set('action', 'do_nothing');
|
||||
record.set('action_name', 'do_nothing');
|
||||
this.send('save', record);
|
||||
},
|
||||
|
||||
block: function(record) {
|
||||
record.set('action', 'block');
|
||||
record.set('action_name', 'block');
|
||||
this.send('save', record);
|
||||
},
|
||||
|
||||
|
|
|
@ -9,20 +9,20 @@
|
|||
**/
|
||||
Discourse.ScreenedIpAddress = Discourse.Model.extend({
|
||||
actionName: function() {
|
||||
return I18n.t("admin.logs.screened_ips.actions." + this.get('action'));
|
||||
}.property('action'),
|
||||
return I18n.t("admin.logs.screened_ips.actions." + this.get('action_name'));
|
||||
}.property('action_name'),
|
||||
|
||||
isBlocked: function() {
|
||||
return (this.get('action') === 'block');
|
||||
}.property('action'),
|
||||
return (this.get('action_name') === 'block');
|
||||
}.property('action_name'),
|
||||
|
||||
actionIcon: function() {
|
||||
if (this.get('action') === 'block') {
|
||||
if (this.get('action_name') === 'block') {
|
||||
return this.get('blockIcon');
|
||||
} else {
|
||||
return this.get('doNothingIcon');
|
||||
}
|
||||
}.property('action'),
|
||||
}.property('action_name'),
|
||||
|
||||
blockIcon: function() {
|
||||
return 'icon-ban-circle';
|
||||
|
@ -33,9 +33,9 @@ Discourse.ScreenedIpAddress = Discourse.Model.extend({
|
|||
}.property(),
|
||||
|
||||
save: function() {
|
||||
return Discourse.ajax("/admin/logs/screened_ip_addresses/" + this.get('id') + ".json", {
|
||||
type: 'PUT',
|
||||
data: {ip_address: this.get('ip_address'), action_name: this.get('action')}
|
||||
return Discourse.ajax("/admin/logs/screened_ip_addresses" + (this.id ? '/' + this.id : '') + ".json", {
|
||||
type: this.id ? 'PUT' : 'POST',
|
||||
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>
|
||||
|
||||
{{screened-ip-address-form action="recordAdded"}}
|
||||
<br/>
|
||||
|
||||
{{#if loading}}
|
||||
<div class='admin-loading'>{{i18n loading}}</div>
|
||||
{{else}}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
//= require ./env
|
||||
|
||||
// probe framework first
|
||||
//= require ./discourse/components/probes.js
|
||||
//= require ./discourse/lib/probes.js
|
||||
|
||||
// Externals we need to load first
|
||||
|
||||
|
|
|
@ -1,5 +1,22 @@
|
|||
Discourse.DiscourseBreadcrumbsComponent = Ember.Component.extend({
|
||||
classNames: ['category-breadcrumb'],
|
||||
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'),
|
||||
foregroundColors: ['FFFFFF', '000000'],
|
||||
|
||||
parentCategories: function() {
|
||||
return Discourse.Category.list().filter(function (c) {
|
||||
return !c.get('parentCategory');
|
||||
});
|
||||
}.property(),
|
||||
|
||||
onShow: function() {
|
||||
this.changeSize();
|
||||
this.titleChanged();
|
||||
|
@ -122,17 +128,27 @@ Discourse.EditCategoryController = Discourse.ObjectController.extend(Discourse.M
|
|||
},
|
||||
|
||||
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);
|
||||
model.set('parentCategory', parentCategory);
|
||||
var newSlug = Discourse.Category.slugFor(this.get('model'));
|
||||
|
||||
this.get('model').save().then(function(result) {
|
||||
// success
|
||||
categoryController.send('closeModal');
|
||||
Discourse.URL.redirectTo("/category/" + Discourse.Category.slugFor(result.category));
|
||||
}, function(errors) {
|
||||
// errors
|
||||
if(errors.length === 0) errors.push(I18n.t("category.creation_error"));
|
||||
categoryController.displayErrors(errors);
|
||||
categoryController.set('saving', false);
|
||||
self.send('closeModal');
|
||||
Discourse.URL.redirectTo("/category/" + newSlug);
|
||||
}, function(error) {
|
||||
|
||||
if (error && error.responseText) {
|
||||
self.flash($.parseJSON(error.responseText).errors[0]);
|
||||
} else {
|
||||
self.flash(I18n.t('generic_error'));
|
||||
}
|
||||
|
||||
self.set('saving', false);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -147,8 +163,14 @@ Discourse.EditCategoryController = Discourse.ObjectController.extend(Discourse.M
|
|||
// success
|
||||
self.send('closeModal');
|
||||
Discourse.URL.redirectTo("/categories");
|
||||
}, function(jqXHR){
|
||||
// error
|
||||
}, function(error){
|
||||
|
||||
if (error && error.responseText) {
|
||||
self.flash($.parseJSON(error.responseText).errors[0]);
|
||||
} else {
|
||||
self.flash(I18n.t('generic_error'));
|
||||
}
|
||||
|
||||
self.send('showModal');
|
||||
self.displayErrors([I18n.t("category.delete_error")]);
|
||||
self.set('deleting', false);
|
||||
|
|
|
@ -7,32 +7,34 @@
|
|||
@module Discourse
|
||||
**/
|
||||
Discourse.ListController = Discourse.Controller.extend({
|
||||
categoryBinding: 'topicList.category',
|
||||
categoryBinding: "topicList.category",
|
||||
canCreateCategory: false,
|
||||
canCreateTopic: false,
|
||||
needs: ['composer', 'modal', 'listTopics'],
|
||||
needs: ["composer", "modal", "listTopics"],
|
||||
|
||||
availableNavItems: function() {
|
||||
var loggedOn = !!Discourse.User.current();
|
||||
var category = this.get("category");
|
||||
|
||||
return Discourse.SiteSettings.top_menu.split("|").map(function(i) {
|
||||
return Discourse.NavItem.fromText(i, {
|
||||
loggedOn: loggedOn
|
||||
loggedOn: loggedOn,
|
||||
category: category
|
||||
});
|
||||
}).filter(function(i) {
|
||||
return i !== null;
|
||||
return i !== null && !(category && i.get("name").indexOf("categor") === 0);
|
||||
});
|
||||
}.property(),
|
||||
}.property("category"),
|
||||
|
||||
createTopicText: function() {
|
||||
if (this.get('category.name')) {
|
||||
if (this.get("category.name")) {
|
||||
return I18n.t("topic.create_in", {
|
||||
categoryName: this.get('category.name')
|
||||
categoryName: this.get("category.name")
|
||||
});
|
||||
} else {
|
||||
return I18n.t("topic.create");
|
||||
}
|
||||
}.property('category.name'),
|
||||
}.property("category.name"),
|
||||
|
||||
/**
|
||||
Refresh our current topic list
|
||||
|
@ -132,7 +134,11 @@ Discourse.ListController = Discourse.Controller.extend({
|
|||
} else {
|
||||
return false;
|
||||
}
|
||||
}.property('category')
|
||||
}.property('category'),
|
||||
|
||||
categories: function() {
|
||||
return Discourse.Category.list();
|
||||
}.property()
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -7,8 +7,15 @@
|
|||
@module Discourse
|
||||
**/
|
||||
Discourse.StaticController = Discourse.Controller.extend({
|
||||
needs: ['header'],
|
||||
path: null,
|
||||
|
||||
showLoginButton: function() {
|
||||
return this.get('path') === '/login';
|
||||
}.property('path'),
|
||||
|
||||
loadPath: function(path) {
|
||||
this.set('path', path);
|
||||
var staticController = this;
|
||||
this.set('content', null);
|
||||
|
||||
|
|
|
@ -29,12 +29,32 @@ Handlebars.registerHelper('shorten', function(property, options) {
|
|||
@for Handlebars
|
||||
**/
|
||||
Handlebars.registerHelper('topicLink', function(property, options) {
|
||||
var title, topic;
|
||||
topic = Ember.Handlebars.get(this, property, options);
|
||||
title = topic.get('fancy_title') || topic.get('title');
|
||||
return "<a href='" + (topic.get('lastUnreadUrl')) + "' class='title'>" + title + "</a>";
|
||||
var topic = Ember.Handlebars.get(this, property, options),
|
||||
title = topic.get('fancy_title') || topic.get('title');
|
||||
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
|
||||
|
||||
|
@ -42,21 +62,16 @@ Handlebars.registerHelper('topicLink', function(property, options) {
|
|||
@for Handlebars
|
||||
**/
|
||||
Handlebars.registerHelper('categoryLink', function(property, options) {
|
||||
var allowUncategorized = options.hash && options.hash.allowUncategorized;
|
||||
var category = Ember.Handlebars.get(this, property, options);
|
||||
return new Handlebars.SafeString(Discourse.Utilities.categoryLink(category, allowUncategorized));
|
||||
return categoryLinkHTML(Ember.Handlebars.get(this, property, options), options);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
Produces a bound link to a category
|
||||
|
||||
@method boundCategoryLink
|
||||
@for Handlebars
|
||||
**/
|
||||
Ember.Handlebars.registerBoundHelper('boundCategoryLink', function(category) {
|
||||
return new Handlebars.SafeString(Discourse.Utilities.categoryLink(category));
|
||||
});
|
||||
Ember.Handlebars.registerBoundHelper('boundCategoryLink', categoryLinkHTML);
|
||||
|
||||
/**
|
||||
Produces a link to a route with support for i18n on the title
|
||||
|
|
|
@ -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) {
|
||||
if (!template) { return ""; }
|
||||
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'));
|
||||
}.property('name'),
|
||||
|
||||
unreadUrl: function() {
|
||||
return this.get('url') + '/unread';
|
||||
}.property('url'),
|
||||
|
||||
newUrl: function() {
|
||||
return this.get('url') + '/new';
|
||||
}.property('url'),
|
||||
|
||||
style: function() {
|
||||
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'),
|
||||
permissions: this.get('permissionsForUpdate'),
|
||||
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'
|
||||
});
|
||||
|
|
|
@ -31,18 +31,32 @@ Discourse.NavItem = Discourse.Model.extend({
|
|||
|
||||
// href from this item
|
||||
href: function() {
|
||||
return Discourse.getURL("/") + this.get('filterMode');
|
||||
}.property('filterMode'),
|
||||
|
||||
// href from this item
|
||||
filterMode: function() {
|
||||
var name = this.get('name');
|
||||
if( name.split('/')[0] === 'category' ) {
|
||||
return Discourse.getURL("/") + 'category/' + this.get('categorySlug');
|
||||
return 'category/' + this.get('categorySlug');
|
||||
} 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'),
|
||||
|
||||
count: function() {
|
||||
var state = this.get('topicTrackingState');
|
||||
if (state) {
|
||||
return state.lookupCount(this.get('name'));
|
||||
return state.lookupCount(this.get('name'), this.get('category'));
|
||||
}
|
||||
}.property('topicTrackingState.messageCount'),
|
||||
|
||||
|
@ -71,7 +85,8 @@ Discourse.NavItem.reopenClass({
|
|||
opts = {
|
||||
name: name,
|
||||
hasIcon: name === "unread" || name === "favorited",
|
||||
filters: split.splice(1)
|
||||
filters: split.splice(1),
|
||||
category: opts.category
|
||||
};
|
||||
|
||||
return Discourse.NavItem.create(opts);
|
||||
|
|
|
@ -146,7 +146,6 @@ Discourse.TopicList.reopenClass({
|
|||
return Ember.RSVP.resolve(list);
|
||||
}
|
||||
session.setProperties({topicList: null, topicListScrollPos: null});
|
||||
|
||||
return Discourse.TopicList.find(filter, menuItem.get('excludeCategory'));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -159,15 +159,16 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
|
|||
return count;
|
||||
},
|
||||
|
||||
lookupCount: function(name){
|
||||
lookupCount: function(name, category){
|
||||
var categoryName = Em.get(category, "name");
|
||||
if(name==="new") {
|
||||
return this.countNew();
|
||||
return this.countNew(categoryName);
|
||||
} else if(name==="unread") {
|
||||
return this.countUnread();
|
||||
return this.countUnread(categoryName);
|
||||
} else {
|
||||
var category = name.split("/")[1];
|
||||
if(category) {
|
||||
return this.countCategory(category);
|
||||
categoryName = name.split("/")[1];
|
||||
if(categoryName) {
|
||||
return this.countCategory(categoryName);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -26,8 +26,10 @@ Discourse.Route.buildRoutes(function() {
|
|||
Discourse.ListController.filters.forEach(function(filter) {
|
||||
router.route(filter, { path: "/" + filter });
|
||||
router.route(filter, { path: "/" + filter + "/more" });
|
||||
router.route(filter + "Category", { path: "/category/:slug/" + filter });
|
||||
router.route(filter + "Category", { path: "/category/:slug/" + filter + "/more" });
|
||||
router.route(filter + "Category", { path: "/category/:slug/l/" + filter });
|
||||
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'),
|
||||
urlId = Discourse.Category.slugFor(category),
|
||||
self = this;
|
||||
categorySlug = Discourse.Category.slugFor(category),
|
||||
self = this,
|
||||
filter = this.filter || "latest",
|
||||
url = "category/" + categorySlug + "/l/" + filter;
|
||||
|
||||
listController.set('filterMode', "category/" + urlId);
|
||||
listController.load("category/" + urlId).then(function(topicList) {
|
||||
listController.set('filterMode', url);
|
||||
listController.load(url).then(function(topicList) {
|
||||
listController.setProperties({
|
||||
canCreateTopic: topicList.get('can_create_topic'),
|
||||
category: category
|
||||
|
|
|
@ -1,17 +1,12 @@
|
|||
<li>
|
||||
<a href="/">{{title}}</a>
|
||||
<i class='icon icon-caret-right first-caret'></i>
|
||||
{{discourse-categorydrop parentCategory=category categories=parentCategories}}
|
||||
</li>
|
||||
<li>
|
||||
{{discourse-categorydrop parentCategory=category category=targetCategory categories=childCategories}}
|
||||
</li>
|
||||
|
||||
{{#if parentCategory}}
|
||||
<li>
|
||||
{{discourse-categorydrop category=parentCategory categories=categories}}
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
{{#if category}}
|
||||
<li>
|
||||
{{discourse-categorydrop category=category}}
|
||||
{{boundCategoryLink category}}
|
||||
</li>
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
{{categoryLink category}}
|
||||
{{#if category}}
|
||||
{{boundCategoryLink category allowUncategorized=true}}
|
||||
{{else}}
|
||||
<a href='/' class='badge-category home' {{bindAttr style="badgeStyle"}}><i class='icon icon-home'></i></a>
|
||||
{{/if}}
|
||||
|
||||
{{#if categories}}
|
||||
<button {{action expand}}><i class='icon icon-caret-right'></i></button>
|
||||
{{/if}}
|
||||
<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}}
|
||||
|
|
|
@ -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}}
|
||||
<li class='category'>
|
||||
{{categoryLink this}}
|
||||
{{categoryLink this allowUncategorized=true}}
|
||||
<b>{{unbound topic_count}}</b></a>
|
||||
</li>
|
||||
{{/each}}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
<div id='list-controls'>
|
||||
<div class='list-controls'>
|
||||
<div class="container">
|
||||
|
||||
{{#if category}}
|
||||
{{discourse-breadcrumbs category=category categories=categories}}
|
||||
{{/if}}
|
||||
|
||||
<ul class="nav nav-pills" id='category-filter'>
|
||||
{{each availableNavItems itemViewClass="Discourse.NavItemView"}}
|
||||
</ul>
|
||||
|
|
|
@ -41,9 +41,11 @@
|
|||
{{/if}}
|
||||
</td>
|
||||
|
||||
{{#unless controller.category}}
|
||||
<td class='category'>
|
||||
{{categoryLink category}}
|
||||
</td>
|
||||
{{/unless}}
|
||||
|
||||
<td class='posters'>
|
||||
{{#each posters}}
|
||||
|
|
|
@ -10,10 +10,6 @@
|
|||
</button>
|
||||
{{/if}}
|
||||
|
||||
{{#if category}}
|
||||
{{discourse-breadcrumbs title=Discourse.SiteSettings.title category=category categories=categories}}
|
||||
{{/if}}
|
||||
|
||||
<table id='topic-list'>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -23,7 +19,9 @@
|
|||
<th class='main-link'>
|
||||
{{i18n topic.title}}
|
||||
</th>
|
||||
{{#unless category}}
|
||||
<th class='category'>{{i18n category_title}}</th>
|
||||
{{/unless}}
|
||||
<th class='posters'>{{i18n top_contributors}}</th>
|
||||
<th class='num posts'>{{i18n posts}}</th>
|
||||
<th class='num likes'>{{i18n likes}}</th>
|
||||
|
|
|
@ -22,10 +22,10 @@
|
|||
{{/if}}
|
||||
{{categoryLink this allowUncategorized=true}}
|
||||
{{#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 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}}
|
||||
<div class='featured-users'>
|
||||
{{#each featured_users}}
|
||||
|
|
|
@ -21,6 +21,11 @@
|
|||
{{textField value=name placeholderKey="category.name_placeholder" maxlength="50"}}
|
||||
</section>
|
||||
|
||||
<section class='field'>
|
||||
<label>{{i18n category.parent}}</label>
|
||||
{{categoryChooser valueAttribute="id" value=parent_category_id categories=parentCategories}}
|
||||
</section>
|
||||
|
||||
{{#unless isUncategorized}}
|
||||
<section class='field'>
|
||||
<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}}
|
|
@ -2,6 +2,10 @@
|
|||
<div class='contents clearfix body-page'>
|
||||
{{#if content}}
|
||||
{{{content}}}
|
||||
|
||||
{{#if showLoginButton}}
|
||||
<button class="btn btn-primary" {{action showLogin}}>{{i18n log_in}}</button>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class='spinner'>{{i18n loading}}</div>
|
||||
{{/if}}
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
|
||||
<section class='controls'>
|
||||
{{#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>
|
||||
{{i18n user.private_message}}
|
||||
</button>
|
||||
|
@ -84,14 +84,14 @@
|
|||
{{/if}}
|
||||
|
||||
{{#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 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}}
|
||||
|
||||
{{#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>
|
||||
|
|
|
@ -12,14 +12,16 @@ Discourse.CategoryChooserView = Discourse.ComboboxView.extend({
|
|||
dataAttributes: ['name', 'color', 'text_color', 'description_text', 'topic_count'],
|
||||
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() {
|
||||
this._super();
|
||||
// TODO perhaps allow passing a param in to select if we need full or not
|
||||
|
||||
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;
|
||||
}));
|
||||
if (!this.get('categories')) {
|
||||
this.set('categories', Discourse.Category.list());
|
||||
}
|
||||
},
|
||||
|
||||
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() {
|
||||
var flashMessage = this.get('controller.flashMessage');
|
||||
if (flashMessage) {
|
||||
|
|
|
@ -14,7 +14,7 @@ Discourse.NavItemView = Discourse.View.extend({
|
|||
hidden: Em.computed.not('content.visible'),
|
||||
count: Ember.computed.alias('content.count'),
|
||||
shouldRerender: Discourse.View.renderIfChanged('count'),
|
||||
active: Discourse.computed.propertyEqual('contentNameSlug', 'controller.filterMode'),
|
||||
active: Discourse.computed.propertyEqual('content.filterMode', 'controller.filterMode'),
|
||||
|
||||
title: function() {
|
||||
var categoryName, extra, name;
|
||||
|
@ -27,13 +27,6 @@ Discourse.NavItemView = Discourse.View.extend({
|
|||
return I18n.t("filters." + name + ".help", extra);
|
||||
}.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() {
|
||||
var categoryName, extra, name;
|
||||
|
|
|
@ -17,6 +17,10 @@ Discourse.PopupInputTipView = Discourse.View.extend({
|
|||
bouncePixels: 6,
|
||||
bounceDelay: 100,
|
||||
|
||||
click: function(e) {
|
||||
this.set('shownAt', false);
|
||||
},
|
||||
|
||||
good: function() {
|
||||
return !this.get('validation.failed');
|
||||
}.property('validation'),
|
||||
|
@ -25,10 +29,6 @@ Discourse.PopupInputTipView = Discourse.View.extend({
|
|||
return this.get('validation.failed');
|
||||
}.property('validation'),
|
||||
|
||||
hide: function() {
|
||||
this.set('shownAt', false);
|
||||
},
|
||||
|
||||
bounce: function() {
|
||||
if( this.get('shownAt') ) {
|
||||
var $elem = this.$();
|
||||
|
|
|
@ -46,7 +46,8 @@ Discourse.QuoteButtonView = Discourse.View.extend({
|
|||
$(document)
|
||||
.on("mousedown.quote-button", function(e) {
|
||||
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
|
||||
// (allows anyone to `extend` their selection using shift+click)
|
||||
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');
|
||||
if (category) {
|
||||
opts.catLink = Discourse.Utilities.categoryLink(category);
|
||||
opts.catLink = Discourse.HTML.categoryLink(category);
|
||||
} else {
|
||||
opts.catLink = "<a href=\"" + Discourse.getURL("/categories") + "\">" + (I18n.t("topic.browse_all_categories")) + "</a>";
|
||||
}
|
||||
|
|
|
@ -43,11 +43,11 @@
|
|||
// Stuff we need to load first
|
||||
//= require ./discourse/mixins/scrolling
|
||||
//= require_tree ./discourse/mixins
|
||||
//= require ./discourse/components/markdown
|
||||
//= require ./discourse/components/computed
|
||||
//= require ./discourse/lib/markdown
|
||||
//= require ./discourse/lib/computed
|
||||
//= require ./discourse/views/view
|
||||
//= require ./discourse/views/container_view
|
||||
//= require ./discourse/components/debounce
|
||||
//= require ./discourse/lib/debounce
|
||||
//= require ./discourse/models/model
|
||||
//= require ./discourse/models/user_action
|
||||
//= require ./discourse/models/composer
|
||||
|
@ -63,9 +63,10 @@
|
|||
//= require ./discourse/dialects/dialect
|
||||
//= require_tree ./discourse/dialects
|
||||
//= require_tree ./discourse/controllers
|
||||
//= require_tree ./discourse/components
|
||||
//= require_tree ./discourse/lib
|
||||
//= require_tree ./discourse/models
|
||||
//= require_tree ./discourse/views
|
||||
//= require_tree ./discourse/components
|
||||
//= require_tree ./discourse/helpers
|
||||
//= require_tree ./discourse/templates
|
||||
//= 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 {
|
||||
.ip_address {
|
||||
width: 110px;
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Notification badge
|
||||
// --------------------------------------------------
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@import "common/foundation/variables";
|
||||
@import "common/foundation/mixins";
|
||||
@import "foundation/variables";
|
||||
@import "foundation/mixins";
|
||||
|
||||
.popup-tip {
|
||||
position: absolute;
|
||||
|
@ -16,14 +16,15 @@
|
|||
&.hide, &.good {
|
||||
display: none;
|
||||
}
|
||||
a.close {
|
||||
.close {
|
||||
float: right;
|
||||
color: $black;
|
||||
opacity: 0.5;
|
||||
font-size: 15px;
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
a.close:hover {
|
||||
.close:hover {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
|
@ -164,6 +164,19 @@
|
|||
border-bottom: 1px solid #bbb;
|
||||
|
||||
}
|
||||
|
||||
.category-combobox {
|
||||
width: 430px;
|
||||
|
||||
.chzn-drop {
|
||||
left: -9000px;
|
||||
width: 428px;
|
||||
}
|
||||
.chzn-search input {
|
||||
width: 378px;
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
// List controls
|
||||
// --------------------------------------------------
|
||||
|
||||
#list-controls {
|
||||
.list-controls {
|
||||
.nav {
|
||||
float: left;
|
||||
margin-bottom: 15px;
|
||||
|
@ -340,52 +340,102 @@
|
|||
// Misc. stuff
|
||||
// --------------------------------------------------
|
||||
|
||||
#main {
|
||||
#list-controls {
|
||||
.badge-category {
|
||||
display: inline-block;
|
||||
background-color: yellow;
|
||||
margin: 8px 0 0 8px;
|
||||
float: left;
|
||||
}
|
||||
clear: both;
|
||||
.list-controls {
|
||||
.home {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
}
|
||||
#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;}
|
||||
|
||||
|
||||
|
||||
.empty-topic-list {
|
||||
padding: 10px;
|
||||
}
|
||||
.unseen {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
color: lighten($red, 10%);
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
}
|
||||
.empty-topic-list {
|
||||
padding: 10px;
|
||||
}
|
||||
#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");
|
||||
};
|
||||
.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");
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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;
|
||||
margin-top: 10px;
|
||||
padding: 5px;
|
||||
height: 32px;
|
||||
|
||||
.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;
|
||||
}
|
||||
.title-input .popup-tip {
|
||||
width: 300px;
|
||||
left: -8px;
|
||||
margin-top: 8px;
|
||||
width: 240px;
|
||||
right: 5px;
|
||||
}
|
||||
.category-input .popup-tip {
|
||||
width: 240px;
|
||||
left: 432px;
|
||||
top: -7px;
|
||||
right: 5px;
|
||||
}
|
||||
.textarea-wrapper .popup-tip {
|
||||
top: 28px;
|
||||
}
|
||||
button.btn.no-text {
|
||||
margin: 7px 0 0 5px;
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
margin: 20px 15px;
|
||||
|
||||
// Consistent vertical spacing
|
||||
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 {
|
||||
margin: 5px;
|
||||
.nav {
|
||||
float: left;
|
||||
|
@ -239,48 +239,64 @@
|
|||
// Misc. stuff
|
||||
// --------------------------------------------------
|
||||
|
||||
#main {
|
||||
#list-controls {
|
||||
.badge-category {
|
||||
display: inline-block;
|
||||
background-color: yellow;
|
||||
margin: 8px 0 0 8px;
|
||||
float: left;
|
||||
}
|
||||
clear: both;
|
||||
.list-controls {
|
||||
.home {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
}
|
||||
#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 {
|
||||
|
||||
.badge-category {
|
||||
margin-top: 6px;
|
||||
padding: 4px 10px;
|
||||
display: inline-block;
|
||||
text-indent: -9999em;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: {
|
||||
image: image-url("posted.png");
|
||||
};
|
||||
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;
|
||||
.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;
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
if @screened_ip_address.update_attributes(allowed_params)
|
||||
render json: success_json
|
||||
|
|
|
@ -90,7 +90,7 @@ class CategoriesController < ApplicationController
|
|||
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
|
||||
|
||||
|
|
|
@ -10,14 +10,11 @@ class SessionController < ApplicationController
|
|||
params.require(:login)
|
||||
params.require(:password)
|
||||
|
||||
login = params[:login].strip
|
||||
login = login[1..-1] if login[0] == "@"
|
||||
login = params[:login].strip
|
||||
password = params[:password]
|
||||
login = login[1..-1] if login[0] == "@"
|
||||
|
||||
if login =~ /@/
|
||||
@user = User.where(email: Email.downcase(login)).first
|
||||
else
|
||||
@user = User.where(username_lower: login.downcase).first
|
||||
end
|
||||
@user = User.find_by_username_or_email(login)
|
||||
|
||||
if @user.present?
|
||||
|
||||
|
@ -28,7 +25,7 @@ class SessionController < ApplicationController
|
|||
end
|
||||
|
||||
# If their password is correct
|
||||
if @user.confirm_password?(params[:password])
|
||||
if @user.confirm_password?(password)
|
||||
|
||||
if @user.is_banned?
|
||||
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
|
||||
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?
|
||||
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)
|
||||
|
|
|
@ -29,8 +29,9 @@ class TopicsController < ApplicationController
|
|||
return wordpress if params[:best].present?
|
||||
|
||||
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
|
||||
@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>
|
||||
# (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
|
||||
|
||||
|
@ -63,25 +64,21 @@ class TopicsController < ApplicationController
|
|||
params.require(:best)
|
||||
params.require(:topic_id)
|
||||
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(
|
||||
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"
|
||||
)
|
||||
|
||||
@topic_view = TopicView.new(params[:topic_id], current_user, opts)
|
||||
discourse_expires_in 1.minute
|
||||
|
||||
wordpress_serializer = TopicViewWordpressSerializer.new(@topic_view, scope: guardian, root: false)
|
||||
render_json_dump(wordpress_serializer)
|
||||
end
|
||||
|
||||
|
||||
def posts
|
||||
params.require(:topic_id)
|
||||
params.require(:post_ids)
|
||||
|
@ -97,41 +94,31 @@ class TopicsController < ApplicationController
|
|||
|
||||
def update
|
||||
topic = Topic.where(id: params[:topic_id]).first
|
||||
title, archetype = params[:title], params[:archetype]
|
||||
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
|
||||
if current_user.admin?
|
||||
topic.archetype = "regular" if params[:archetype] == 'regular'
|
||||
end
|
||||
topic.archetype = "regular" if current_user.admin? && archetype == 'regular'
|
||||
|
||||
success = false
|
||||
Topic.transaction do
|
||||
success = topic.save
|
||||
success = topic.change_category(params[:category]) if success
|
||||
end
|
||||
|
||||
# this is used to return the title to the client as it may have been
|
||||
# changed by "TextCleaner"
|
||||
if success
|
||||
render_serialized(topic, BasicTopicSerializer)
|
||||
else
|
||||
render_json_error(topic)
|
||||
end
|
||||
success ? render_serialized(topic, BasicTopicSerializer) : render_json_error(topic)
|
||||
end
|
||||
|
||||
def similar_to
|
||||
params.require(:title)
|
||||
params.require(:raw)
|
||||
title, raw = params[:title], params[:raw]
|
||||
|
||||
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
|
||||
[:title, :raw].each { |key| check_length_of(key, params[key]) }
|
||||
|
||||
# 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
|
||||
end
|
||||
topics = Topic.similar_to(title, raw, current_user).to_a if Topic.count_exceeds_minimum?
|
||||
|
||||
render_serialized(topics, BasicTopicSerializer)
|
||||
end
|
||||
|
@ -139,11 +126,13 @@ class TopicsController < ApplicationController
|
|||
def status
|
||||
params.require(:status)
|
||||
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])
|
||||
@topic = Topic.where(id: params[:topic_id].to_i).first
|
||||
check_for_status_presence(:status, status)
|
||||
@topic = Topic.where(id: topic_id).first
|
||||
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
|
||||
end
|
||||
|
||||
|
@ -203,14 +192,7 @@ class TopicsController < ApplicationController
|
|||
end
|
||||
|
||||
def invite
|
||||
username_or_email = params[:user]
|
||||
if username_or_email
|
||||
# provides a level of protection for hashes
|
||||
params.require(:user)
|
||||
else
|
||||
params.require(:email)
|
||||
username_or_email = params[:email]
|
||||
end
|
||||
username_or_email = params[:user] ? fetch_username : fetch_email
|
||||
|
||||
topic = Topic.where(id: params[:topic_id]).first
|
||||
guardian.ensure_can_invite_to!(topic)
|
||||
|
@ -347,4 +329,27 @@ class TopicsController < ApplicationController
|
|||
topic.move_posts(current_user, post_ids_including_replies, args)
|
||||
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
|
||||
|
|
|
@ -22,6 +22,7 @@ class AdminDashboardData
|
|||
|
||||
def problems
|
||||
[ rails_env_check,
|
||||
ruby_version_check,
|
||||
host_names_check,
|
||||
gc_checks,
|
||||
sidekiq_check || queue_size_check,
|
||||
|
@ -161,6 +162,10 @@ class AdminDashboardData
|
|||
I18n.t('dashboard.notification_email_warning') if SiteSetting.notification_email.blank?
|
||||
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
|
||||
# that should be reported on the admin dashboard:
|
||||
|
|
|
@ -187,10 +187,10 @@ SQL
|
|||
|
||||
def parent_category_validator
|
||||
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
|
||||
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
|
||||
|
||||
|
|
|
@ -59,7 +59,8 @@ class Topic < ActiveRecord::Base
|
|||
validates :category_id, :presence => true ,:exclusion => {:in => [SiteSetting.uncategorized_category_id]},
|
||||
:if => Proc.new { |t|
|
||||
(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
|
||||
scope :private_messages, lambda {
|
||||
where(archetype: Archetype::private_message)
|
||||
where(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})
|
||||
true
|
||||
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
|
||||
end
|
||||
end
|
||||
|
@ -182,6 +183,10 @@ class Topic < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def self.count_exceeds_minimum?
|
||||
count > SiteSetting.minimum_topics_similar
|
||||
end
|
||||
|
||||
def best_post
|
||||
posts.order('score desc').limit(1).first
|
||||
end
|
||||
|
|
|
@ -124,19 +124,19 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.find_by_username_or_email(username_or_email)
|
||||
conditions = if username_or_email.include?('@')
|
||||
{ email: Email.downcase(username_or_email) }
|
||||
if username_or_email.include?('@')
|
||||
find_by_email(username_or_email)
|
||||
else
|
||||
{ username_lower: username_or_email.downcase }
|
||||
find_by_username(username_or_email)
|
||||
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
|
||||
raise Discourse::TooManyMatches
|
||||
else
|
||||
users.first
|
||||
end
|
||||
def self.find_by_username(username)
|
||||
where(username_lower: username.downcase).first
|
||||
end
|
||||
|
||||
def enqueue_welcome_message(message_type)
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
class ScreenedIpAddressSerializer < ApplicationSerializer
|
||||
attributes :id,
|
||||
:ip_address,
|
||||
:action,
|
||||
:action_name,
|
||||
:match_count,
|
||||
:last_match_at,
|
||||
:created_at
|
||||
|
||||
def action
|
||||
def action_name
|
||||
ScreenedIpAddress.actions.key(object.action_type).to_s
|
||||
end
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
class SpamRulesEnforcer
|
||||
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
# The exclamation point means that this method may make big changes to posts and users.
|
||||
def self.enforce!(arg)
|
||||
SpamRulesEnforcer.new(arg).enforce!
|
||||
|
@ -15,73 +13,13 @@ class SpamRulesEnforcer
|
|||
end
|
||||
|
||||
def enforce!
|
||||
# TODO: once rules are in their own classes, invoke them from here in priority order
|
||||
if @user
|
||||
block_user if block?
|
||||
SpamRule::AutoBlock.new(@user).perform
|
||||
end
|
||||
if @post
|
||||
flag_sockpuppet_users if SiteSetting.flag_sockpuppets and reply_is_from_sockpuppet?
|
||||
SpamRule::FlagSockpuppets.new(@post).perform
|
||||
end
|
||||
true
|
||||
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
|
||||
|
|
|
@ -18,6 +18,14 @@ module Discourse
|
|||
# Application configuration should go into files in config/initializers
|
||||
# -- 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 'js_locale_helper'
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
production:
|
||||
first_thing:
|
||||
# 1. Permissions on postgres box
|
||||
- source: /.cloud66/scripts/permissions.sh
|
||||
- source: /config/cloud/cloud66/scripts/permissions.sh
|
||||
destination: /tmp/scripts/permissions.sh
|
||||
target: postgresql
|
||||
apply_during: build_only
|
||||
|
@ -16,25 +16,29 @@ production:
|
|||
owner: postgres
|
||||
after_checkout:
|
||||
# 3. Copy Procfile
|
||||
- source: /.cloud66/files/Procfile
|
||||
- source: /config/cloud/cloud66/files/Procfile
|
||||
destination: <%= ENV['RAILS_STACK_PATH'] %>/Procfile
|
||||
target: rails
|
||||
run_on: all_servers
|
||||
# 4. Copy redis settings
|
||||
- source: /.cloud66/files/redis.yml
|
||||
- source: /config/cloud/cloud66/files/redis.yml
|
||||
destination: <%= ENV['RAILS_STACK_PATH'] %>/config/redis.yml
|
||||
target: rails
|
||||
parse: false
|
||||
run_on: all_servers
|
||||
# 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
|
||||
target: rails
|
||||
run_on: all_servers
|
||||
# 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
|
||||
target: rails
|
||||
run_on: all_servers
|
||||
after_rails:
|
||||
# 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
|
||||
target: rails
|
||||
apply_during: build_only
|
||||
|
@ -42,21 +46,21 @@ production:
|
|||
sudo: true
|
||||
last_thing:
|
||||
# 8. KILL DB
|
||||
- source: /.cloud66/scripts/kill_db.sh
|
||||
- source: /config/cloud/cloud66/scripts/kill_db.sh
|
||||
destination: /tmp/scripts/kill_db.sh
|
||||
target: postgresql
|
||||
apply_during: build_only
|
||||
execute: true
|
||||
sudo: true
|
||||
# 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
|
||||
target: rails
|
||||
apply_during: build_only
|
||||
execute: true
|
||||
sudo: true
|
||||
# 10. Import database image
|
||||
- source: /.cloud66/scripts/import_prod.sh
|
||||
- source: /config/cloud/cloud66/scripts/import_prod.sh
|
||||
destination: /tmp/scripts/import_prod.sh
|
||||
target: postgresql
|
||||
apply_during: build_only
|
||||
|
@ -64,14 +68,14 @@ production:
|
|||
owner: postgres
|
||||
run_as: postgres
|
||||
# 11. Migrate database
|
||||
- source: /.cloud66/scripts/migrate.sh
|
||||
- source: /config/cloud/cloud66/scripts/migrate.sh
|
||||
destination: /tmp/migrate.sh
|
||||
target: rails
|
||||
apply_during: build_only
|
||||
execute: true
|
||||
sudo: true
|
||||
# 12. Curl script
|
||||
- source: /.cloud66/scripts/curl.sh
|
||||
- source: /config/cloud/cloud66/scripts/curl.sh
|
||||
destination: /tmp/curl.sh
|
||||
target: rails
|
||||
apply_during: build_only
|
||||
|
@ -80,7 +84,7 @@ production:
|
|||
staging:
|
||||
first_thing:
|
||||
# 1. Permissions on postgres box
|
||||
- source: /.cloud66/scripts/permissions.sh
|
||||
- source: /config/cloud/cloud66/scripts/permissions.sh
|
||||
destination: /tmp/scripts/permissions.sh
|
||||
target: postgresql
|
||||
apply_during: build_only
|
||||
|
@ -95,25 +99,29 @@ staging:
|
|||
owner: postgres
|
||||
after_checkout:
|
||||
# 3. Copy Procfile
|
||||
- source: /.cloud66/files/Procfile
|
||||
- source: /config/cloud/cloud66/files/Procfile
|
||||
destination: <%= ENV['RAILS_STACK_PATH'] %>/Procfile
|
||||
target: rails
|
||||
run_on: all_servers
|
||||
# 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
|
||||
target: rails
|
||||
parse: false
|
||||
run_on: all_servers
|
||||
# 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
|
||||
target: rails
|
||||
run_on: all_servers
|
||||
# 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
|
||||
target: rails
|
||||
run_on: all_servers
|
||||
after_rails:
|
||||
# 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
|
||||
target: rails
|
||||
apply_during: build_only
|
||||
|
@ -121,21 +129,21 @@ staging:
|
|||
sudo: true
|
||||
last_thing:
|
||||
# 8. KILL DB
|
||||
- source: /.cloud66/scripts/kill_db.sh
|
||||
- source: /config/cloud/cloud66/scripts/kill_db.sh
|
||||
destination: /tmp/scripts/kill_db.sh
|
||||
target: postgresql
|
||||
apply_during: build_only
|
||||
execute: true
|
||||
sudo: true
|
||||
# 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
|
||||
target: rails
|
||||
apply_during: build_only
|
||||
execute: true
|
||||
sudo: true
|
||||
# 10. Import database image
|
||||
- source: /.cloud66/scripts/import_prod.sh
|
||||
- source: /config/cloud/cloud66/scripts/import_prod.sh
|
||||
destination: /tmp/scripts/import_prod.sh
|
||||
target: postgresql
|
||||
apply_during: build_only
|
||||
|
@ -143,14 +151,14 @@ staging:
|
|||
owner: postgres
|
||||
run_as: postgres
|
||||
# 11. Migrate database
|
||||
- source: /.cloud66/scripts/migrate.sh
|
||||
- source: /config/cloud/cloud66/scripts/migrate.sh
|
||||
destination: /tmp/migrate.sh
|
||||
target: rails
|
||||
apply_during: build_only
|
||||
execute: true
|
||||
sudo: true
|
||||
# 12. Curl script
|
||||
- source: /.cloud66/scripts/curl.sh
|
||||
- source: /config/cloud/cloud66/scripts/curl.sh
|
||||
destination: /tmp/curl.sh
|
||||
target: rails
|
||||
apply_during: build_only
|
||||
|
@ -159,7 +167,7 @@ staging:
|
|||
development:
|
||||
first_thing:
|
||||
# 1. Permissions on postgres box
|
||||
- source: /.cloud66/scripts/permissions.sh
|
||||
- source: /config/cloud/cloud66/scripts/permissions.sh
|
||||
destination: /tmp/scripts/permissions.sh
|
||||
target: postgresql
|
||||
apply_during: build_only
|
||||
|
@ -174,21 +182,24 @@ development:
|
|||
owner: postgres
|
||||
after_checkout:
|
||||
# 3. Copy Procfile
|
||||
- source: /.cloud66/files/Procfile
|
||||
- source: /config/cloud/cloud66/files/Procfile
|
||||
destination: <%= ENV['RAILS_STACK_PATH'] %>/Procfile
|
||||
target: rails
|
||||
run_on: all_servers
|
||||
# 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
|
||||
target: rails
|
||||
parse: false
|
||||
run_on: all_servers
|
||||
# 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
|
||||
target: rails
|
||||
run_on: all_servers
|
||||
after_rails:
|
||||
# 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
|
||||
target: rails
|
||||
apply_during: build_only
|
||||
|
@ -196,21 +207,21 @@ development:
|
|||
sudo: true
|
||||
last_thing:
|
||||
# 7. KILL DB
|
||||
- source: /.cloud66/scripts/kill_db.sh
|
||||
- source: /config/cloud/cloud66/scripts/kill_db.sh
|
||||
destination: /tmp/scripts/kill_db.sh
|
||||
target: postgresql
|
||||
apply_during: build_only
|
||||
execute: true
|
||||
sudo: true
|
||||
# 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
|
||||
target: rails
|
||||
apply_during: build_only
|
||||
execute: true
|
||||
sudo: true
|
||||
# 9. Import database image
|
||||
- source: /.cloud66/scripts/import_dev.sh
|
||||
- source: /config/cloud/cloud66/scripts/import_dev.sh
|
||||
destination: /tmp/scripts/import_dev.sh
|
||||
target: postgresql
|
||||
apply_during: build_only
|
||||
|
@ -218,14 +229,14 @@ development:
|
|||
owner: postgres
|
||||
run_as: postgres
|
||||
# 10. Migrate database
|
||||
- source: /.cloud66/scripts/migrate.sh
|
||||
- source: /config/cloud/cloud66/scripts/migrate.sh
|
||||
destination: /tmp/migrate.sh
|
||||
target: rails
|
||||
apply_during: build_only
|
||||
execute: true
|
||||
sudo: true
|
||||
# 11. Curl script
|
||||
- source: /.cloud66/scripts/curl.sh
|
||||
- source: /config/cloud/cloud66/scripts/curl.sh
|
||||
destination: /tmp/curl.sh
|
||||
target: rails
|
||||
apply_during: build_only
|
||||
|
|
|
@ -11,7 +11,7 @@ production:
|
|||
### If you change this setting you will need to
|
||||
### - restart sidekiq if you change this setting
|
||||
### - 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:
|
||||
adapter: postgresql
|
||||
|
|
|
@ -975,6 +975,7 @@ en:
|
|||
add_permission: "Add Permission"
|
||||
this_year: "this year"
|
||||
position: "position"
|
||||
parent: "Parent Category"
|
||||
|
||||
flagging:
|
||||
title: 'Why are you flagging this post?'
|
||||
|
@ -1267,11 +1268,15 @@ en:
|
|||
url: "URL"
|
||||
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}?"
|
||||
actions:
|
||||
block: "Block"
|
||||
do_nothing: "Allow"
|
||||
form:
|
||||
label: "New:"
|
||||
ip_address: "IP address"
|
||||
add: "Add"
|
||||
|
||||
impersonate:
|
||||
title: "Impersonate User"
|
||||
|
|
|
@ -576,6 +576,7 @@ fr:
|
|||
total: total messages
|
||||
current: message courant
|
||||
notifications:
|
||||
title: ''
|
||||
reasons:
|
||||
'3_2': 'Vous recevrez des notifications car vous suivez attentivement cette discussion.'
|
||||
'3_1': 'Vous recevrez des notifications car vous avez créé cette discussion.'
|
||||
|
@ -858,7 +859,11 @@ fr:
|
|||
security: "Sécurité"
|
||||
auto_close_label: "Fermer automatiquement après :"
|
||||
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:
|
||||
title: 'Pourquoi voulez-vous signaler ce message ?'
|
||||
action: 'Signaler ce message'
|
||||
|
|
|
@ -122,6 +122,7 @@ ru:
|
|||
log_in: 'Войти'
|
||||
age: Возраст
|
||||
last_post: 'Последнее сообщение'
|
||||
joined: Присоединен
|
||||
admin_title: Админка
|
||||
flags_title: Жалобы
|
||||
show_more: 'показать еще'
|
||||
|
@ -202,18 +203,25 @@ ru:
|
|||
sent_by_user: 'Отправлено пользователем <a href=''{{userUrl}}''>{{user}}</a>'
|
||||
sent_by_you: 'Отправлено <a href=''{{userUrl}}''>Вами</a>'
|
||||
user_action_groups:
|
||||
'1': 'Отдано симпатий'
|
||||
'2': 'Получено симпатий'
|
||||
'3': Закладки
|
||||
'4': Темы
|
||||
'5': Сообщения
|
||||
'6': Ответы
|
||||
'7': Упоминания
|
||||
'9': Цитаты
|
||||
'10': Избранное
|
||||
'11': Изменения
|
||||
'12': 'Отправленные'
|
||||
'13': Входящие
|
||||
"1": 'Отдал симпатий'
|
||||
"2": 'Получил симпатий'
|
||||
"3": Закладки
|
||||
"4": Темы
|
||||
"5": Сообщения
|
||||
"6": Ответы
|
||||
"7": Упоминания
|
||||
"9": Цитаты
|
||||
"10": Избранное
|
||||
"11": Изменения
|
||||
"12": 'Отправленные'
|
||||
"13": Входящие
|
||||
categories:
|
||||
category: Категория
|
||||
posts: Сообщения
|
||||
topics: Темы
|
||||
latest: Последние
|
||||
latest_by: 'последние по'
|
||||
toggle_ordering: 'изменить сортировку'
|
||||
user:
|
||||
said: '{{username}} писал(а):'
|
||||
profile: Профайл
|
||||
|
@ -294,7 +302,7 @@ ru:
|
|||
title: 'Пароль еще раз'
|
||||
last_posted: 'Последнее сообщение'
|
||||
last_emailed: 'Последнее письмо'
|
||||
last_seen: 'Посл. визит'
|
||||
last_seen: 'Последний визит'
|
||||
created: 'Регистрация'
|
||||
log_out: 'Выйти'
|
||||
website: 'Веб-сайт'
|
||||
|
@ -487,8 +495,8 @@ ru:
|
|||
link_optional_text: 'необязательное название'
|
||||
quote_title: Цитата
|
||||
quote_text: Цитата
|
||||
code_title: 'Фрагмент кода'
|
||||
code_text: 'вводите код здесь'
|
||||
code_title: 'Форматированный текст'
|
||||
code_text: 'введите форматированный текст'
|
||||
upload_title: Загрузить
|
||||
upload_description: 'введите здесь описание загружаемого объекта'
|
||||
olist_title: 'Нумерованный список'
|
||||
|
@ -528,6 +536,8 @@ ru:
|
|||
remote_tip_with_attachments: 'Введите адрес изображения или файла в формате http://example.com/file.ext (список доступных расширений: {{authorized_extensions}}).'
|
||||
local_tip: 'кликните для выбора изображения с вашего устройства'
|
||||
local_tip_with_attachments: 'кликните для выбора изображения с вашего устройства (доступные расширения: {{authorized_extensions}})'
|
||||
hint: '(вы так же можете перетащить объект в редактор для его загрузки)'
|
||||
hint_for_chrome: '(вы так же можете перетащить или вставить изображение в редактор для его загрузки)'
|
||||
uploading: Загрузка
|
||||
search:
|
||||
title: 'поиск по темам, сообщениям, пользователям или категориям'
|
||||
|
@ -576,6 +586,16 @@ ru:
|
|||
private_message: 'Написать личное сообщение'
|
||||
list: Темы
|
||||
new: 'новая тема'
|
||||
new_topics:
|
||||
one: '1 новая тема'
|
||||
other: '{{count}} новых тем'
|
||||
few: '{{count}} новых темы'
|
||||
many: '{{count}} новых тем'
|
||||
unread_topics:
|
||||
one: '1 непрочитанная тема'
|
||||
other: '{{count}} непрочитанных тем'
|
||||
few: '{{count}} непрочитанных темы'
|
||||
many: '{{count}} непрочитанных тем'
|
||||
title: Тема
|
||||
loading_more: 'Загружаю темы...'
|
||||
loading: 'Загружаю тему...'
|
||||
|
@ -630,16 +650,16 @@ ru:
|
|||
notifications:
|
||||
title: ' '
|
||||
reasons:
|
||||
'3_2': 'Вы будете получать уведомления, потому что вы наблюдаете за темой.'
|
||||
'3_1': 'Вы будете получать уведомления, потому что вы создали тему.'
|
||||
'3': 'Вы будете получать уведомления, потому что вы наблюдаете за темой.'
|
||||
'2_4': 'Вы будете получать уведомления, потому что вы ответили в теме.'
|
||||
'2_2': 'Вы будете получать уведомления, потому что вы отслеживаете тему.'
|
||||
'2': 'Вы будете получать уведомления, потому что вы <a href="/users/{{username}}/preferences">читали тему</a>.'
|
||||
'1': 'Вы получите уведомление, только если кто-нибудь упомянет вас по @name или ответит на ваше сообщение.'
|
||||
'1_2': 'Вы получите уведомление, только если кто-нибудь упомянет вас по @name или ответит на ваше сообщение.'
|
||||
'0': 'Вы не получаете уведомления по теме.'
|
||||
'0_2': 'Вы не получаете уведомления по теме.'
|
||||
"3_2": 'Вы будете получать уведомления, потому что вы наблюдаете за темой.'
|
||||
"3_1": 'Вы будете получать уведомления, потому что вы создали тему.'
|
||||
"3": 'Вы будете получать уведомления, потому что вы наблюдаете за темой.'
|
||||
"2_4": 'Вы будете получать уведомления, потому что вы ответили в теме.'
|
||||
"2_2": 'Вы будете получать уведомления, потому что вы отслеживаете тему.'
|
||||
"2": 'Вы будете получать уведомления, потому что вы <a href="/users/{{username}}/preferences">читали тему</a>.'
|
||||
"1": 'Вы получите уведомление, только если кто-нибудь упомянет вас по @name или ответит на ваше сообщение.'
|
||||
"1_2": 'Вы получите уведомление, только если кто-нибудь упомянет вас по @name или ответит на ваше сообщение.'
|
||||
"0": 'Вы не получаете уведомления по теме.'
|
||||
"0_2": 'Вы не получаете уведомления по теме.'
|
||||
watching:
|
||||
title: Наблюдение
|
||||
description: 'то же самое, что и режим отслеживания, но вы дополнительно будете получать уведомления обо всех новых сообщениях.'
|
||||
|
@ -939,7 +959,7 @@ ru:
|
|||
category:
|
||||
can: 'может… '
|
||||
none: '(без категории)'
|
||||
choose: 'Select a category…'
|
||||
choose: 'Выберете категорию…'
|
||||
edit: изменить
|
||||
edit_long: 'Изменить категорию'
|
||||
view: 'Просмотр тем по категориям'
|
||||
|
@ -970,13 +990,16 @@ ru:
|
|||
auto_close_label: 'Закрыть тему через:'
|
||||
edit_permissions: 'Изменить права доступа'
|
||||
add_permission: 'Добавить права'
|
||||
this_year: 'в год'
|
||||
position: местоположение
|
||||
parent: 'Родительская категория'
|
||||
flagging:
|
||||
title: 'Выберите действие над сообщением'
|
||||
action: 'Пожаловаться'
|
||||
take_action: 'Принять меры'
|
||||
notify_action: Отправить
|
||||
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: 'Да, удалить спамера'
|
||||
cant: 'Извините, но вы не можете сейчас послать жалобу.'
|
||||
custom_placeholder_notify_user: 'Почему это сообщение побудило вас обратиться к этому пользователю напрямую и в частном порядке? Будьте конкретны, будьте конструктивны и всегда доброжелательны.'
|
||||
|
@ -1159,12 +1182,18 @@ ru:
|
|||
delete_confirm: 'Удалить данную группу?'
|
||||
delete_failed: 'Невозможно удалить группу. Если это автоматически созданная группа, то она не может быть удалена.'
|
||||
api:
|
||||
generate_master: 'Сгенерировать ключ API'
|
||||
none: 'Отсутствует ключ API.'
|
||||
user: Пользователь
|
||||
title: API
|
||||
long_title: 'Информация об API'
|
||||
key: Ключ
|
||||
generate: 'Сгенерировать ключ API'
|
||||
regenerate: 'Сгенерировать ключ API заново'
|
||||
key: 'Ключ API'
|
||||
generate: Сгенерировать
|
||||
regenerate: Перегенерировать
|
||||
revoke: Отозвать
|
||||
confirm_regen: 'Вы уверены, что хотите заменить ключ API?'
|
||||
confirm_revoke: 'Вы уверены, что хотите отозвать этот ключ?'
|
||||
info_html: 'Ваш API ключ позволит вам создавать и обновлять темы, используя JSON calls.'
|
||||
all_users: 'Все пользователи'
|
||||
note_html: 'Никому <strong>не сообщайте</strong> эти ключи, Тот, у кого они есть, сможет создавать сообщения, выдавая себя за любого пользователя форума.'
|
||||
customize:
|
||||
title: Оформление
|
||||
|
@ -1210,6 +1239,9 @@ ru:
|
|||
last_match_at: 'Последнее совпадение'
|
||||
match_count: Совпадения
|
||||
ip_address: IP
|
||||
delete: Удалить
|
||||
edit: Изменить
|
||||
save: Сохранить
|
||||
screened_actions:
|
||||
block: блокировать
|
||||
do_nothing: 'ничего не делать'
|
||||
|
@ -1244,6 +1276,17 @@ ru:
|
|||
title: 'Отслеживаемые ссылки'
|
||||
description: 'Список ссылок от пользователей, которые были идентифицированы как спамеры.'
|
||||
url: URL
|
||||
screened_ips:
|
||||
title: 'Наблюдаемые IP адреса'
|
||||
description: 'IP адреса за которыми вести наблюдение. Используйте "Разрешить" для добавления IP адреса в белый список.'
|
||||
delete_confirm: 'Вы уверены, что хотите удалить правило для %{ip_address}?'
|
||||
actions:
|
||||
block: Заблокировать
|
||||
do_nothing: Разрешить
|
||||
form:
|
||||
label: 'Новые:'
|
||||
ip_address: 'IP адрес'
|
||||
add: Добавить
|
||||
impersonate:
|
||||
title: 'Представиться как пользователь'
|
||||
username_or_email: 'Имя пользователя или Email'
|
||||
|
@ -1325,7 +1368,7 @@ ru:
|
|||
reputation: Репутация
|
||||
permissions: Права
|
||||
activity: Активность
|
||||
like_count: 'Получено симпатий'
|
||||
like_count: 'Получил симпатий'
|
||||
private_topics_count: 'Частные темы'
|
||||
posts_read_count: 'Прочитано сообщений'
|
||||
post_count: 'Создано сообщений'
|
||||
|
@ -1344,8 +1387,8 @@ ru:
|
|||
few: 'Пользователь не может быть удален, если он зарегистрирован больше чем %{count} дня назад и у него есть сообщения. Удалите все сообщения перед удалением пользователя.'
|
||||
many: 'Пользователь не может быть удален, если он зарегистрирован больше чем %{count} дней назад и у него есть сообщения. Удалите все сообщения перед удалением пользователя.'
|
||||
delete_confirm: 'Вы уверены, что хотите удалить пользователя? Это действие необратимо!'
|
||||
delete_and_block: '<b>Да</b>, и <b>заблокировать</b> повторную регистрацию с данного адреса'
|
||||
delete_dont_block: '<b>Да</b>, но <b>разрешить</b> повторную регистрацию с данного адреса'
|
||||
delete_and_block: '<b>Да</b>, и <b>запретить</b> регистрацию с данного email и IP адреса'
|
||||
delete_dont_block: '<b>Да</b>, но <b>разрешить</b> регистрацию с данного email и IP адреса'
|
||||
deleted: 'Пользователь удален.'
|
||||
delete_failed: 'При удалении пользователя возникла ошибка. Для удаления пользователя необходимо сначала удалить все его сообщения.'
|
||||
send_activation_email: 'Послать активационное письмо'
|
||||
|
|
|
@ -975,6 +975,7 @@ zh_CN:
|
|||
add_permission: "添加权限"
|
||||
this_year: "今年"
|
||||
position: "位置"
|
||||
parent: "上级分类"
|
||||
|
||||
flagging:
|
||||
title: '为何要报告本帖?'
|
||||
|
@ -1267,11 +1268,15 @@ zh_CN:
|
|||
url: "URL"
|
||||
screened_ips:
|
||||
title: "被屏蔽的IP"
|
||||
description: "受监视的IP地址。"
|
||||
description: "受监视的IP地址,使用“放行”可将IP地址加入白名单。"
|
||||
delete_confirm: "确定要撤销对IP地址为 %{ip_address} 的规则?"
|
||||
actions:
|
||||
block: "阻挡"
|
||||
do_nothing: "放行"
|
||||
form:
|
||||
label: "新:"
|
||||
ip_address: "IP地址"
|
||||
add: "添加"
|
||||
|
||||
impersonate:
|
||||
title: "假冒用户"
|
||||
|
|
|
@ -159,7 +159,9 @@ en:
|
|||
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.]"
|
||||
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:
|
||||
newuser:
|
||||
title: "new user"
|
||||
|
@ -416,6 +418,7 @@ en:
|
|||
|
||||
dashboard:
|
||||
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."
|
||||
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>.'
|
||||
|
|
|
@ -699,7 +699,7 @@ fr:
|
|||
|
||||
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.
|
||||
|
||||
|
|
|
@ -100,6 +100,8 @@ ru:
|
|||
name: 'Название категории'
|
||||
post:
|
||||
raw: Текст сообщения
|
||||
user:
|
||||
ip_address: ''
|
||||
errors:
|
||||
messages:
|
||||
is_invalid: 'слишком короткий'
|
||||
|
@ -109,6 +111,10 @@ ru:
|
|||
attributes:
|
||||
archetype:
|
||||
cant_send_pm: 'Извините, вы не можете посылать личные сообщения данному пользователю.'
|
||||
user:
|
||||
attributes:
|
||||
ip_address:
|
||||
signup_not_allowed: 'Регистрация с данной учетной записью запрещена.'
|
||||
user_profile:
|
||||
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>'
|
||||
|
@ -116,6 +122,9 @@ ru:
|
|||
topic_prefix: 'Описание категории %{category}'
|
||||
replace_paragraph: '[Замените данный текст кратким описанием новой категории. Это описание будет отображаться в списке категорий, поэтому постарайтесь сделать его коротким (не более 200 символов).]'
|
||||
post_template: "%{replace_paragraph}\n\nВ данное поле введите более подробное описание категории, а также возможные правила опубликования в ней тем.\n\nНесколько аспектов, которые следует учитывать:\n\n- Для чего нужна данная категория? Почему люди выберут данную категорию для размещения своей темы?\n\n- Чем данная категория отличается от тех, которые у нас уже есть?\n\n- Нужна ли нам эта категория?\n\n- Стоит ли нам объединить ее с другой категорией или разбить на несколько?\n"
|
||||
errors:
|
||||
self_parent: 'Подкатегория не может быть родительской для самой себя.'
|
||||
depth: 'Вы не можете иметь вложенные подкатегории.'
|
||||
trust_levels:
|
||||
newuser:
|
||||
title: 'новый пользователь'
|
||||
|
@ -408,6 +417,7 @@ ru:
|
|||
num_clicks: Переходов
|
||||
dashboard:
|
||||
rails_env_warning: 'Ваш сервер работает в режиме %{env}.'
|
||||
ruby_version_warning: 'Вы используете версию Ruby 2.0.0, у которой имеются известные проблемы. Обновите версию до patch-247 или более новую.'
|
||||
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>.'
|
||||
sidekiq_warning: 'Sidekiq не запущен. Сейчас многие задачи, такие как отправка электронных писем, выполняются асинхронно. Пожалуйста, убедитесь, что хотя бы один процесс sidekiq запущен. <a href="https://github.com/mperham/sidekiq">Узнайте больше о Sidekiq здесь</a>.'
|
||||
|
@ -518,9 +528,12 @@ ru:
|
|||
new_topic_duration_minutes: 'Глобальное количество минут по умолчанию, в течение которых тема оценивается как новая, пользователи могут переназначать (-1 - всегда, -2 – для последнего посещения)'
|
||||
flags_required_to_hide_post: 'Количество жалоб, по достижении которого сообщение автоматически скрывается, и личное сообщение об этом посылается автору (0 - никогда)'
|
||||
cooldown_minutes_after_hiding_posts: 'Количество минут, которое должен подождать пользователь перед редактированием сообщения скрытого по жалобам'
|
||||
max_topics_in_first_day: 'Максимальное количество тем, которое пользователь может создать в первый день на сайте'
|
||||
max_replies_in_first_day: 'Максимальное количество ответов, которое пользователь может сделать в первый день на сайте'
|
||||
num_flags_to_block_new_user: 'Если сообщения нового пользователя получат данное количество флагов от различных пользователей, скрыть все сообщения пользователя и отказать пользователю в публикации новых сообщений. 0 отключает функцию.'
|
||||
num_users_to_block_new_user: 'Если сообщения нового пользователя получат флаги от данного количества различных пользователей, скрыть все сообщения пользователя и отказать пользователю в публикации новых сообщений. 0 отключает функцию.'
|
||||
notify_mods_when_user_blocked: 'Отправить сообщение всем модераторам, если пользователь заблокирован автоматически.'
|
||||
flag_sockpuppets: 'Если новый пользователь (например, зарегистрированный за последние 24 часа), который начал тему и новый пользователь, который ответил в теме, имеют одинаковые IP адреса, помечать оба сообщения как спам.'
|
||||
traditional_markdown_linebreaks: 'Использовать стандартные разрывы строк в Markdown, вместо двух пробелов'
|
||||
post_undo_action_window_mins: 'Количество секунд, в течение которых пользователь может отменить действие («Мне нравится», «Жалоба» и т.д.)'
|
||||
must_approve_users: 'Администраторы должны одобрять учетные записи всех новых пользователей для того, чтобы они получили доступ'
|
||||
|
@ -578,6 +591,8 @@ ru:
|
|||
max_topics_per_day: 'Максимальное количество тем, которое пользователь может создать в день'
|
||||
max_private_messages_per_day: 'Максимальное количество личных сообщений, которое пользователь может послать в день'
|
||||
suggested_topics: 'Количество рекомендованных тем, отображаемых внизу текущей темы'
|
||||
clean_up_uploads: 'Удалить неиспользуемые загрузки для предотвращения хранения нелегального контента. ВНИМАНИЕ: рекомендуется сделать резервную копию директории /uploads перед включением данной настройки.'
|
||||
uploads_grace_period_in_hours: 'Период (в часах) после которого неопубликованные вложения удаляются.'
|
||||
enable_s3_uploads: 'Размещать загруженные файлы на Amazon S3'
|
||||
s3_upload_bucket: 'Наименование Amazon S3 bucket в который будут загружаться файлы. ВНИМАНИЕ: имя должно быть в нижнем регистре (см. http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html)'
|
||||
s3_access_key_id: 'Amazon S3 access key для загрузки и хранения изображений'
|
||||
|
@ -613,6 +628,7 @@ ru:
|
|||
min_title_similar_length: 'Минимальная длина названия темы, при которой тема будет проверена на наличие похожих'
|
||||
min_body_similar_length: 'Минимальная длина тела сообщения, при которой оно будет проверено на наличие похожих тем'
|
||||
category_colors: 'Разделенный чертой (|) список дозволенных hexadecimal цветов для категорий'
|
||||
enable_wide_category_list: 'Включить традиционный полноразмерный список категорий.'
|
||||
max_image_size_kb: 'Максимальный размер изображений для загрузки пользователем в КБ – убедитесь, что вы так же настроили лимит в nginx (client_max_body_size) / apache или прокси.'
|
||||
max_attachment_size_kb: 'Максимальный размер файлов для загрузки пользователем в кб – убедитесь, что вы настроили лимит также в nginx (client_max_body_size) / apache или proxy.'
|
||||
authorized_extensions: 'Список расширений файлов, разрешенных к загрузке, разделенный вертикальной чертой (|)'
|
||||
|
@ -718,6 +734,8 @@ ru:
|
|||
email:
|
||||
not_allowed: 'недопустимый почтовый домен. Пожалуйста, используйте другой адрес.'
|
||||
blocked: 'не разрешено.'
|
||||
ip_address:
|
||||
blocked: 'блокирован.'
|
||||
invite_mailer:
|
||||
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"
|
||||
|
@ -756,8 +774,8 @@ ru:
|
|||
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"
|
||||
spam_post_blocked:
|
||||
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"
|
||||
subject_template: 'Сообщения нового пользователя %{username} блокированы за повторяющиеся ссылки'
|
||||
text_body_template: "Это автоматическое сообщение для информирования вас о том, что новый пользователь [%{username}](%{base_url}%{user_url}) пытался создать множество сообщений с ссылками на %{domains}, однако они были заблокированы для предотвращения спама. Пользователь по прежнему может создавать новые сообщения, которые не ссылаются на %{domains}.\n\nПожалуйста [проверьте действия пользователя](%{base_url}%{user_url}).\n\nПороговое значение может быть изменено в настройке сайта `newuser_spam_host_threshold`.\n"
|
||||
unblocked:
|
||||
subject_template: 'Учетная запись разблокирована'
|
||||
text_body_template: "Здравствуйте!\n\nЭто автоматическое сообщение сайта %{site_name}. Ваш аккаунт был разблокирован. Теперь вы можете создавать новые темы и отвечать в них.\n"
|
||||
|
@ -839,3 +857,6 @@ ru:
|
|||
fetch_failure: 'Извините, во время извлечения изображения произошла ошибка.'
|
||||
unknown_image_type: 'Файл, который вы загружаете, не является изображением.'
|
||||
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": {
|
||||
"command": "dcomponent"
|
||||
},
|
||||
"app/assets/javascripts/discourse/lib/*.js": {
|
||||
"command": "dlib"
|
||||
},
|
||||
"app/assets/javascripts/discourse/routes/*.js": {
|
||||
"command": "droute"
|
||||
},
|
||||
|
|
|
@ -69,7 +69,7 @@ Discourse::Application.routes.draw do
|
|||
scope '/logs' do
|
||||
resources :staff_action_logs, 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]
|
||||
end
|
||||
|
||||
|
@ -210,8 +210,10 @@ Discourse::Application.routes.draw do
|
|||
get "#{filter}" => "list##{filter}"
|
||||
get "#{filter}/more" => "list##{filter}"
|
||||
|
||||
get "category/:category/#{filter}" => "list##{filter}"
|
||||
get "category/:category/#{filter}/more" => "list##{filter}"
|
||||
get "category/:category/l/#{filter}" => "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
|
||||
|
||||
get 'category/:parent_category/:category' => 'list#category', as: 'category_list_parent'
|
||||
|
|
|
@ -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…
Reference in New Issue