Merge branch 'master' of github.com:discourse/discourse

This commit is contained in:
Sam 2013-12-20 16:17:52 +11:00
commit f5f09933df
31 changed files with 325 additions and 82 deletions

@ -31,7 +31,7 @@ Discourse.CreateAccountController = Discourse.Controller.extend(Discourse.ModalF
}.property('authOptions.auth_provider'), }.property('authOptions.auth_provider'),
passwordInstructions: function() { passwordInstructions: function() {
return I18n.t('user.password.instructions', {count: 6}); // TODO: soon to be configurable return I18n.t('user.password.instructions', {count: Discourse.SiteSettings.min_password_length});
}.property(), }.property(),
// Validate the name // Validate the name
@ -273,7 +273,7 @@ Discourse.CreateAccountController = Discourse.Controller.extend(Discourse.ModalF
} }
// If too short // If too short
if (password.length < 6) { if (password.length < Discourse.SiteSettings.min_password_length) {
return Discourse.InputValidation.create({ return Discourse.InputValidation.create({
failed: true, failed: true,
reason: I18n.t('user.password.too_short') reason: I18n.t('user.password.too_short')

@ -17,7 +17,6 @@ var parser = window.BetterMarkdown,
@method initializeDialects @method initializeDialects
**/ **/
function initializeDialects() { function initializeDialects() {
Discourse.Dialect.trigger('register', {dialect: dialect, MD: MD});
MD.buildBlockOrder(dialect.block); MD.buildBlockOrder(dialect.block);
MD.buildInlinePatterns(dialect.inline); MD.buildInlinePatterns(dialect.inline);
initialized = true; initialized = true;

@ -128,10 +128,13 @@ Discourse.TopicRoute = Discourse.Route.extend({
editingTopic: false editingTopic: false
}); });
Discourse.TopicRoute.trigger('setupTopicController', this);
this.controllerFor('header').setProperties({ this.controllerFor('header').setProperties({
topic: model, topic: model,
showExtraInfo: false showExtraInfo: false
}); });
this.controllerFor('composer').set('topic', model); this.controllerFor('composer').set('topic', model);
Discourse.TopicTrackingState.current().trackIncoming('all'); Discourse.TopicTrackingState.current().trackIncoming('all');
controller.subscribe(); controller.subscribe();
@ -142,4 +145,4 @@ Discourse.TopicRoute = Discourse.Route.extend({
}); });
RSVP.EventTarget.mixin(Discourse.TopicRoute);

@ -41,7 +41,7 @@
<h1>{{username}} {{{statusIcon}}}</h1> <h1>{{username}} {{{statusIcon}}}</h1>
<h2>{{name}}</h2> <h2>{{name}}</h2>
<div class='bio'>{{{bio_excerpt}}}</div> <div class='bio'>{{{bio_cooked}}}</div>
{{#if isSuspended}} {{#if isSuspended}}
<div class='suspended'> <div class='suspended'>

@ -41,17 +41,11 @@
} }
} }
@mixin portrait { @mixin mobile-portrait { @media only screen and (max-width : 320px) { @content; } }
@media only screen and (max-width : 320px) { @mixin not-mobile-portrait { @media only screen and (min-width : 321px) { @content; } }
@content; @mixin mobile-landscape { @media only screen and (min-width : 321px) and (max-width : 959px) { @content; } }
} @mixin not-tablet-landscape { @media only screen and (max-width : 959px) { @content; } }
} @mixin tablet-landscape { @media only screen and (min-width : 960px) { @content; } }
@mixin landscape {
@media only screen and (min-width : 321px) {
@content;
}
}
// CSS3 properties // CSS3 properties
// -------------------------------------------------- // --------------------------------------------------

@ -18,8 +18,12 @@
@include box-shadow(3px 3px 3px rgba($black, 0.14)); @include box-shadow(3px 3px 3px rgba($black, 0.14));
h3 {
margin-bottom: 10px;
}
p { p {
margin: 0 0 10px 0; margin-bottom: 10px;
} }
a.close { a.close {

@ -16,7 +16,7 @@ a.loading-onebox {
.onebox-result { .onebox-result {
margin-top: 15px; margin-top: 15px;
padding: 12px 25px 12px 12px; padding: 12px 25px 12px 12px;
border-left: 5px solid #bebebe; border-left: 5px solid #bebebe;
background: #eee; background: #eee;
font-size: 14px; font-size: 14px;
@ -43,7 +43,10 @@ a.loading-onebox {
.onebox-result-body { .onebox-result-body {
padding-top: 5px; padding-top: 5px;
img { img {
max-width:200px; max-width: 100px;
max-height: 80%;
float: left;
margin-right: 10px;
} }
h3, h4 { h3, h4 {
margin: 0px !important; margin: 0px !important;
@ -55,12 +58,6 @@ a.loading-onebox {
code { code {
max-height: 400px; max-height: 400px;
} }
img {
max-width: 30%;
max-height: 80%;
float: left;
margin-right: 10px;
}
.metrics { .metrics {
clear: both; clear: both;
padding-bottom: 25px; padding-bottom: 25px;
@ -76,8 +73,8 @@ a.loading-onebox {
// RottenTomatoes Onebox // RottenTomatoes Onebox
.onebox-result { .onebox-result {
.onebox-result-body { .onebox-result-body {
img.verdict { img.verdict {
float: none; float: none;
margin-right: 7px; margin-right: 7px;
} }
img.popcorn { img.popcorn {

@ -13,10 +13,6 @@
padding: 12px 12px 5px 12px; padding: 12px 12px 5px 12px;
max-width: 350px; max-width: 350px;
h1.new-user a {
color: $dark_gray;
}
h1 { h1 {
font-size: 30px; font-size: 30px;
line-height: 33px; line-height: 33px;
@ -67,4 +63,8 @@
.btn { .btn {
margin: 0 0 7px 0; margin: 0 0 7px 0;
} }
.new-user a {
color: $dark_gray;
}
} }

@ -572,6 +572,10 @@ iframe {
font-size: 36px; font-size: 36px;
} }
.new-user a {
color: $dark_gray;
}
.staff a { .staff a {
padding: 4px; padding: 4px;
margin: -4px 0 0 0; margin: -4px 0 0 0;

@ -39,7 +39,10 @@ a.loading-onebox {
padding-top: 5px; padding-top: 5px;
font-family: Georgia, Times, "Times New Roman", serif; font-family: Georgia, Times, "Times New Roman", serif;
img { img {
max-width:200px; max-width: 100px;
max-height: 80%;
float: left;
margin-right: 10px;
} }
h3, h4 { h3, h4 {
margin: 0px !important; margin: 0px !important;
@ -51,12 +54,6 @@ a.loading-onebox {
code { code {
max-height: 400px; max-height: 400px;
} }
img {
max-width: 30%;
max-height: 80%;
float: left;
margin-right: 10px;
}
.metrics { .metrics {
clear: both; clear: both;
padding-bottom: 25px; padding-bottom: 25px;
@ -72,8 +69,8 @@ a.loading-onebox {
// RottenTomatoes Onebox // RottenTomatoes Onebox
.onebox-result { .onebox-result {
.onebox-result-body { .onebox-result-body {
img.verdict { img.verdict {
float: none; float: none;
margin-right: 7px; margin-right: 7px;
} }
img.popcorn { img.popcorn {

@ -50,7 +50,7 @@
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
tbody tr { > tbody > tr {
background-color: $white; background-color: $white;
&:nth-child(even) { &:nth-child(even) {
background-color: darken($white, 4%); background-color: darken($white, 4%);
@ -163,19 +163,27 @@
// -------------------------------------------------- // --------------------------------------------------
#topic-list.categories { #topic-list.categories {
.badge-category {
display: inline-block;
margin-top: 1px;
}
.category-description {
margin-top: 8px;
}
.featured-users { .featured-users {
@include portrait { @include mobile-portrait {
margin-bottom: 10px; margin-bottom: 10px;
padding-top: 8px; padding-top: 8px;
clear: left; clear: left;
} }
@include landscape { @include not-mobile-portrait {
float: right; float: right;
} }
} }
.latest { .latest {
@include portrait { width: 150px; } @include mobile-portrait { width: 150px; }
@include landscape { width: 270px; } @include mobile-landscape { width: 270px; }
@include tablet-landscape { width: 450px; }
.featured-topic { .featured-topic {
margin: 8px 0; margin: 8px 0;
a.last-posted-at, a.last-posted-at:visited { a.last-posted-at, a.last-posted-at:visited {
@ -184,7 +192,22 @@
} }
} }
.stats { .stats {
display: none; @include not-tablet-landscape { display: none; }
@include tablet-landscape { min-width: 80px; }
}
td.stats {
.unit {
font-size: 11px;
}
}
table.categoryStats {
td {
padding: 2px;
vertical-align: bottom;
line-height: 17px;
&.value { text-align: right; }
&.unit { text-align: left; }
}
} }
} }

@ -433,7 +433,7 @@ iframe {
background-color: lighten(yellow, 35%); background-color: lighten(yellow, 35%);
} }
h3.new-user a[href] { .new-user a {
color: $dark_gray; color: $dark_gray;
} }

@ -172,6 +172,7 @@ class UsersController < ApplicationController
elsif request.put? elsif request.put?
raise Discourse::InvalidParameters.new(:password) unless params[:password].present? raise Discourse::InvalidParameters.new(:password) unless params[:password].present?
@user.password = params[:password] @user.password = params[:password]
@user.password_required!
logon_after_password_reset if @user.save logon_after_password_reset if @user.save
end end
render layout: 'no_js' render layout: 'no_js'

@ -22,7 +22,7 @@ TopicStatusUpdate = Struct.new(:topic, :user) do
topic.update_column status.name, status.enabled? topic.update_column status.name, status.enabled?
end end
if status.manually_closing_topic? && topic.auto_close_at if topic.auto_close_at && (status.reopening_topic? || status.manually_closing_topic?)
topic.reload.set_auto_close(nil).save topic.reload.set_auto_close(nil).save
end end

@ -143,6 +143,7 @@ class User < ActiveRecord::Base
where(username_lower: username.downcase).first where(username_lower: username.downcase).first
end end
def enqueue_welcome_message(message_type) def enqueue_welcome_message(message_type)
return unless SiteSetting.send_welcome_message? return unless SiteSetting.send_welcome_message?
Jobs.enqueue(:send_system_message, user_id: id, message_type: message_type) Jobs.enqueue(:send_system_message, user_id: id, message_type: message_type)
@ -245,11 +246,23 @@ class User < ActiveRecord::Base
@raw_password = password unless password.blank? @raw_password = password unless password.blank?
end end
def password
'' # so that validator doesn't complain that a password attribute doesn't exist
end
# Indicate that this is NOT a passwordless account for the purposes of validation # Indicate that this is NOT a passwordless account for the purposes of validation
def password_required! def password_required!
@password_required = true @password_required = true
end end
def password_required?
!!@password_required
end
def password_validator
PasswordValidator.new(attributes: :password).validate_each(self, :password, @raw_password)
end
def confirm_password?(password) def confirm_password?(password)
return false unless password_hash && salt return false unless password_hash && salt
self.password_hash == hash_password(password, salt) self.password_hash == hash_password(password, salt)
@ -340,6 +353,10 @@ class User < ActiveRecord::Base
topics_allowed.where(archetype: Archetype.private_message).count topics_allowed.where(archetype: Archetype.private_message).count
end end
def posted_too_much_in_topic?(topic_id)
trust_level == TrustLevel.levels[:newuser] && (Post.where(topic_id: topic_id, user_id: id).count >= SiteSetting.newuser_max_replies_per_topic)
end
def bio_excerpt def bio_excerpt
excerpt = PrettyText.excerpt(bio_cooked, 350) excerpt = PrettyText.excerpt(bio_cooked, 350)
return excerpt if excerpt.blank? || has_trust_level?(:basic) return excerpt if excerpt.blank? || has_trust_level?(:basic)
@ -556,12 +573,6 @@ class User < ActiveRecord::Base
end end
end end
def password_validator
if (@raw_password && @raw_password.length < 6) || (@password_required && !@raw_password)
errors.add(:password, "must be 6 letters or longer")
end
end
def send_approval_email def send_approval_email
Jobs.enqueue(:user_email, Jobs.enqueue(:user_email,
type: :signup_after_approval, type: :signup_after_approval,

@ -2,22 +2,21 @@
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel> <channel>
<% lang = SiteSetting.find_by_name('default_locale').try(:value) %> <% lang = SiteSetting.find_by_name('default_locale').try(:value) %>
<% site_email = SiteSetting.find_by_name('contact_email').try(:value) %>
<title><%= @title %></title> <title><%= @title %></title>
<link><%= @link %></link> <link><%= @link %></link>
<description><%= @description %></description> <description><%= @description %></description>
<% if lang %> <%= "<language>#{lang}</language>" if lang %>
<language><%= lang %></language>
<% end %>
<lastBuildDate><%= @topic_list.topics.first.created_at.rfc2822 %></lastBuildDate> <lastBuildDate><%= @topic_list.topics.first.created_at.rfc2822 %></lastBuildDate>
<atom:link href="<%= @atom_link %>" rel="self" type="application/rss+xml" /> <atom:link href="<%= @atom_link %>" rel="self" type="application/rss+xml" />
<% @topic_list.topics.each do |topic| %> <% @topic_list.topics.each do |topic| %>
<% topic_url = Discourse.base_url + topic.relative_url -%> <% topic_url = Discourse.base_url + topic.relative_url -%>
<item> <item>
<title><%= topic.title %></title> <title><%= topic.title %></title>
<author><%= "@#{topic.user.username} (#{topic.user.name})" -%></author> <author><%= "#{site_email} (@#{topic.user.username}#{" #{topic.user.name}" if topic.user.name.present?})" -%></author>
<category><%= topic.category.name %></category> <category><%= topic.category.name %></category>
<description><![CDATA[ <description><![CDATA[
<p><%= t('author_wrote', author: link_to(topic.user.name, topic.user)).html_safe %></p> <p><%= t('author_wrote', author: link_to(topic.user.name, userpage_url(topic.user))).html_safe %></p>
<blockquote> <blockquote>
<%= topic.posts.first.cooked.html_safe %> <%= topic.posts.first.cooked.html_safe %>
</blockquote> </blockquote>

@ -3,23 +3,22 @@
<channel> <channel>
<% topic_url = Discourse.base_url + @topic_view.relative_url %> <% topic_url = Discourse.base_url + @topic_view.relative_url %>
<% lang = SiteSetting.find_by_name('default_locale').try(:value) %> <% lang = SiteSetting.find_by_name('default_locale').try(:value) %>
<% site_email = SiteSetting.find_by_name('contact_email').try(:value) %>
<title><%= @topic_view.title %></title> <title><%= @topic_view.title %></title>
<link><%= topic_url %></link> <link><%= topic_url %></link>
<description><%= @topic_view.posts.first.raw %></description> <description><%= @topic_view.posts.first.raw %></description>
<% if lang %> <%= "<language>#{lang}</language>" if lang %>
<language><%= lang %></language>
<% end %>
<lastBuildDate><%= @topic_view.topic.created_at.rfc2822 %></lastBuildDate> <lastBuildDate><%= @topic_view.topic.created_at.rfc2822 %></lastBuildDate>
<category><%= @topic_view.topic.category.name %></category> <category><%= @topic_view.topic.category.name %></category>
<atom:link href="<%= topic_url %>.rss" rel="self" type="application/rss+xml" /> <atom:link href="<%= topic_url %>.rss" rel="self" type="application/rss+xml" />
<% @topic_view.recent_posts.each do |post| %> <% @topic_view.recent_posts.each do |post| %>
<% next unless post.user %> <% next unless post.user %>
<item> <item>
<title><%= @topic_view.title %> at <%= post.created_at %></title> <title><%= @topic_view.title %></title>
<author><%= "@#{post.user.username} (#{post.user.name})" -%></author> <author><%= "#{site_email} (@#{post.user.username}#{" #{post.user.name}" if post.user.name.present?})" -%></author>
<description><![CDATA[ <description><![CDATA[
<% post_url = Discourse.base_url + post.url %> <% post_url = Discourse.base_url + post.url %>
<p><%= t('author_wrote', author: link_to(post.user.name, post.user)).html_safe %></p> <p><%= t('author_wrote', author: link_to(post.user.name, userpage_url(post.user))).html_safe %></p>
<blockquote> <blockquote>
<%= post.cooked.html_safe %> <%= post.cooked.html_safe %>
</blockquote> </blockquote>

@ -197,7 +197,7 @@ ru:
you_replied_to_topic: '<a href=''{{userUrl}}''>Вы</a> ответили в <a href=''{{topicUrl}}''>теме</a>' you_replied_to_topic: '<a href=''{{userUrl}}''>Вы</a> ответили в <a href=''{{topicUrl}}''>теме</a>'
user_mentioned_user: '<a href=''{{user1Url}}''>{{user}}</a> упомянул <a href=''{{user2Url}}''>{{another_user}}</a>' user_mentioned_user: '<a href=''{{user1Url}}''>{{user}}</a> упомянул <a href=''{{user2Url}}''>{{another_user}}</a>'
user_mentioned_you: '<a href=''{{user1Url}}''>{{user}}</a> упомянул<a href=''{{user2Url}}''>Вас</a>' user_mentioned_you: '<a href=''{{user1Url}}''>{{user}}</a> упомянул<a href=''{{user2Url}}''>Вас</a>'
you_mentioned_user: '<a href=''{{user1Url}}''>Вы</a> упомянули<a href=''{{user2Url}}''>{{another_user}}</a>' you_mentioned_user: '<a href=''{{user1Url}}''>Вы</a> упомянули <a href=''{{user2Url}}''>{{another_user}}</a>'
posted_by_user: 'Размещено пользователем <a href=''{{userUrl}}''>{{user}}</a>' posted_by_user: 'Размещено пользователем <a href=''{{userUrl}}''>{{user}}</a>'
posted_by_you: 'Размещено <a href=''{{userUrl}}''>Вами</a>' posted_by_you: 'Размещено <a href=''{{userUrl}}''>Вами</a>'
sent_by_user: 'Отправлено пользователем <a href=''{{userUrl}}''>{{user}}</a>' sent_by_user: 'Отправлено пользователем <a href=''{{userUrl}}''>{{user}}</a>'
@ -225,6 +225,8 @@ ru:
latest_by: 'последние по' latest_by: 'последние по'
toggle_ordering: 'изменить сортировку' toggle_ordering: 'изменить сортировку'
subcategories: 'Подкатегории:' subcategories: 'Подкатегории:'
total_topics: 'Всего тем: %{count}'
total_posts: 'Всего сообщений: %{count}'
user: user:
said: '{{username}} писал(а):' said: '{{username}} писал(а):'
profile: Профайл profile: Профайл
@ -393,12 +395,14 @@ ru:
month_desc: 'создано тем за последние 30 дней' month_desc: 'создано тем за последние 30 дней'
week: неделя week: неделя
week_desc: 'создано тем за последние 7 дней' week_desc: 'создано тем за последние 7 дней'
day: день
first_post: 'Первое сообщение' first_post: 'Первое сообщение'
mute: Отключить mute: Отключить
unmute: Включить unmute: Включить
summary: summary:
enabled_description: 'Вы просматриваете только популярные сообщения в данной теме. Для просмотра всех сообщений нажмите кнопку ниже.' enabled_description: 'Вы просматриваете только популярные сообщения в данной теме. Для просмотра всех сообщений нажмите кнопку ниже.'
description: 'В теме <b>{{count}}</b> сообщений. Хотите посмотреть только сообщения релевантные теме?' description: 'В теме <b>{{count}}</b> сообщений. Хотите посмотреть только сообщения релевантные теме?'
description_time: 'В теме <b>{{count}}</b> сообщений со средним временем чтения <b>{{readingTime}} минут</b>. Сократить время чтения, отобразив только важные сообщения?'
enable: 'Сводка по теме' enable: 'Сводка по теме'
disable: 'Показать все сообщения' disable: 'Показать все сообщения'
private_message_info: private_message_info:
@ -520,8 +524,10 @@ ru:
help: 'Справка по Markdown' help: 'Справка по Markdown'
toggler: 'скрыть / показать панель редактирования' toggler: 'скрыть / показать панель редактирования'
admin_options_title: 'Дополнительные настройки темы' admin_options_title: 'Дополнительные настройки темы'
auto_close_label: 'Автоматически закрыть тему после:' auto_close_label: 'Автоматически закрыть тему:'
auto_close_units: дней auto_close_units: '(# часов, время, или штамп времени)'
auto_close_examples: 'Например: 24, 17:00, 2013-11-22 14:00'
auto_close_error: 'Пожалуйста, введите правильное значение'
notifications: notifications:
title: 'уведомления об упоминании @name в сообщениях, ответах на ваши сообщения и темы, личные сообщения и т.д.' title: 'уведомления об упоминании @name в сообщениях, ответах на ваши сообщения и темы, личные сообщения и т.д.'
none: 'На данный момент уведомлений нет.' none: 'На данный момент уведомлений нет.'
@ -534,8 +540,8 @@ ru:
liked: '<i title=''liked'' class=''fa fa-heart''></i> {{username}} {{link}}' liked: '<i title=''liked'' class=''fa fa-heart''></i> {{username}} {{link}}'
private_message: '<i class=''fa fa-envelope-o'' title=''private message''></i> {{username}} {{link}}' private_message: '<i class=''fa fa-envelope-o'' title=''private message''></i> {{username}} {{link}}'
invited_to_private_message: '<i class=''fa fa-envelope-o'' title=''private message''></i> {{username}} {{link}}' invited_to_private_message: '<i class=''fa fa-envelope-o'' title=''private message''></i> {{username}} {{link}}'
invitee_accepted: '<i title=''принятое приглашение'' class=''fa fa-sign-in''></i> {{username}} принял ваше приглашение' invitee_accepted: '<i title=''accepted your invitation'' class=''fa fa-sign-in''></i> {{username}} принял ваше приглашение'
moved_post: '<i title=''moved post'' class=''fa fa-arrow-right''></i> {{username}} переместил сообщение в {{link}}' moved_post: '<i title=''moved post'' class=''fa fa-arrow-right''></i> {{username}} перемещено {{link}}'
total_flagged: 'всего сообщений с жалобами' total_flagged: 'всего сообщений с жалобами'
upload_selector: upload_selector:
title: 'Add an image' title: 'Add an image'
@ -653,8 +659,10 @@ ru:
title: 'текущее местоположение в теме' title: 'текущее местоположение в теме'
jump_top: 'перейти к первому сообщению' jump_top: 'перейти к первому сообщению'
jump_bottom: 'перейти к последнему сообщению' jump_bottom: 'перейти к последнему сообщению'
jump_bottom_with_number: 'перейти к сообщению %{post_number}'
total: 'всего сообщений' total: 'всего сообщений'
current: 'текущее сообщение' current: 'текущее сообщение'
position: '%{current} сообщение из %{total}'
notifications: notifications:
title: '&nbsp;' title: '&nbsp;'
reasons: reasons:
@ -778,6 +786,11 @@ ru:
many: '(сообщение отозвано автором и будет автоматически удалено через %{count} часов при отсутствии жалоб)' many: '(сообщение отозвано автором и будет автоматически удалено через %{count} часов при отсутствии жалоб)'
deleted_by: 'Удалено' deleted_by: 'Удалено'
expand_collapse: развернуть/свернуть expand_collapse: развернуть/свернуть
gap:
one: '1 сообщение пропущено'
other: '{{count}} сообщений пропущено'
few: '{{count}} сообщения пропущено'
many: '{{count}} сообщений пропущено'
has_replies: has_replies:
one: ответ one: ответ
other: ответов other: ответов
@ -949,6 +962,25 @@ ru:
other: 'Вы уверены, что хотите удалить эти сообщения?' other: 'Вы уверены, что хотите удалить эти сообщения?'
few: 'Вы уверены, что хотите удалить сообщения?' few: 'Вы уверены, что хотите удалить сообщения?'
many: 'Вы уверены, что хотите удалить сообщения?' many: 'Вы уверены, что хотите удалить сообщения?'
revisions:
controls:
first: 'Начальная версия'
previous: 'Предыдущая версия'
next: 'Следующая версия'
last: 'Последняя версия'
comparing_previous_to_current_out_of_total: '<strong>#{{previous}}</strong> vs. <strong>#{{current}}</strong> (из {{total}})'
displays:
inline:
title: 'Отобразить сообщение с включенными добавлениями и удалениями.'
button: '<i class="fa fa-square-o"></i> HTML'
side_by_side:
title: 'Отобразить сообщение с построчными изменениями'
button: '<i class="fa fa-columns"></i> HTML'
side_by_side_markdown:
title: 'Отобразить вывод с построчными изменениями и разметкой'
button: '<i class="fa fa-columns"></i> Markdown'
details:
edited_by: 'Изменено'
category: category:
can: 'может&hellip; ' can: 'может&hellip; '
none: '(без категории)' none: '(без категории)'
@ -981,6 +1013,7 @@ ru:
already_used: 'Цвет уже используется другой категорией' already_used: 'Цвет уже используется другой категорией'
security: Безопасность security: Безопасность
auto_close_label: 'Закрыть тему через:' auto_close_label: 'Закрыть тему через:'
auto_close_units: часов
edit_permissions: 'Изменить права доступа' edit_permissions: 'Изменить права доступа'
add_permission: 'Добавить права' add_permission: 'Добавить права'
this_year: 'в год' this_year: 'в год'
@ -1135,6 +1168,7 @@ ru:
disagree_title: 'Удалить все жалобы с данного сообщения' disagree_title: 'Удалить все жалобы с данного сообщения'
delete_spammer_title: 'Удалить пользователя и все его сообщения.' delete_spammer_title: 'Удалить пользователя и все его сообщения.'
flagged_by: 'Отмечено' flagged_by: 'Отмечено'
system: Системные
error: 'что-то пошло не так' error: 'что-то пошло не так'
view_message: Ответить view_message: Ответить
no_results: 'Жалоб нет.' no_results: 'Жалоб нет.'
@ -1430,3 +1464,5 @@ ru:
rate_limits: 'Ограничения' rate_limits: 'Ограничения'
developer: Разработчик developer: Разработчик
uncategorized: Без категории uncategorized: Без категории
lightbox:
download: загрузить

@ -185,17 +185,26 @@ zh_CN:
categories: categories:
all: "所有分类" all: "所有分类"
only_category: "只看{{categoryName}}" all_subcategories: "所有子分类"
no_subcategory: "无子分类"
category: "分类" category: "分类"
posts: "帖子" posts: "帖子"
topics: "主题" topics: "主题"
latest: "最新" latest: "最新"
latest_by: "最新发表:" latest_by: "最新发表:"
toggle_ordering: "排序控制" toggle_ordering: "排序控制"
subcategories: "子分类:" subcategories: "子分类:"
topic_stats: "新主题的数量。"
topic_stat_sentence:
one: "过去的%{unit}中有%{count}个新主题。"
other: "过去的%{unit}中有%{count}个新主题。"
post_stats: "新帖子的数量。"
post_stat_sentence:
one: "过去的%{unit}中有%{count}个新帖子。"
other: "过去的%{unit}中有%{count}个新帖子。"
user: user:
said: "{{username}} 说:" said: "{{username}}说:"
profile: "个人简介" profile: "个人简介"
show_profile: "访问个人简介" show_profile: "访问个人简介"
mute: "防打扰" mute: "防打扰"
@ -348,6 +357,7 @@ zh_CN:
title: "密码" title: "密码"
too_short: "你设置的密码太短了。" too_short: "你设置的密码太短了。"
ok: "你设置的密码符合要求。" ok: "你设置的密码符合要求。"
instructions: "至少需要%{count}个字符。"
ip_address: ip_address:
title: "最后使用的IP地址" title: "最后使用的IP地址"
@ -375,6 +385,7 @@ zh_CN:
month_desc: '30天以前发表的主题' month_desc: '30天以前发表的主题'
week: '周' week: '周'
week_desc: '7天以前发表的主题' week_desc: '7天以前发表的主题'
day: '天'
first_post: 第一帖 first_post: 第一帖
mute: 防打扰 mute: 防打扰
@ -1003,6 +1014,7 @@ zh_CN:
add_permission: "添加权限" add_permission: "添加权限"
this_year: "今年" this_year: "今年"
position: "位置" position: "位置"
default_position: "默认位置"
parent: "上级分类" parent: "上级分类"
flagging: flagging:

@ -27,6 +27,8 @@ en:
site_under_maintenance: 'Site is currently undergoing maintenance.' site_under_maintenance: 'Site is currently undergoing maintenance.'
operation_already_running: "An %{operation} is currently running. Can't start a new %{operation} job right now." operation_already_running: "An %{operation} is currently running. Can't start a new %{operation} job right now."
too_many_replies: "Sorry you can't reply any more times in that topic."
too_many_mentions: too_many_mentions:
zero: "Sorry, you can't mention other users." zero: "Sorry, you can't mention other users."
one: "Sorry, you can only mention one other user in a post." one: "Sorry, you can only mention one other user in a post."
@ -132,6 +134,13 @@ en:
Are you sure you're providing adequate time for other people to share their points of view, too? Are you sure you're providing adequate time for other people to share their points of view, too?
too_many_replies: |
### You have reached the reply limit
We're sorry, but new users are temporarily limited to %{newuser_max_replies_per_topic} replies in a single topic.
Instead of adding another reply, please consider editing your previous replies.
activerecord: activerecord:
attributes: attributes:
category: category:
@ -594,6 +603,7 @@ en:
login_required: "Require authentication to read posts" login_required: "Require authentication to read posts"
min_password_length: "Minimum password length."
enable_local_logins: "Enable traditional, local username and password authentication" enable_local_logins: "Enable traditional, local username and password authentication"
enable_local_account_create: "Enable creating new local accounts" enable_local_account_create: "Enable creating new local accounts"
enable_google_logins: "Enable Google authentication" enable_google_logins: "Enable Google authentication"
@ -667,6 +677,7 @@ en:
newuser_max_images: "How many images a new user can add to a post" newuser_max_images: "How many images a new user can add to a post"
newuser_max_attachments: "How many attachments a new user can add to a post" newuser_max_attachments: "How many attachments a new user can add to a post"
newuser_max_mentions_per_post: "Maximum number of @name notifications a new user can use in a post" newuser_max_mentions_per_post: "Maximum number of @name notifications a new user can use in a post"
newuser_max_replies_per_topic: "Maximum number of replies a new user can make in a single topic"
max_mentions_per_post: "Maximum number of @name notifications you can use in a post" max_mentions_per_post: "Maximum number of @name notifications you can use in a post"
create_thumbnails: "Create thumbnails for lightboxed images" create_thumbnails: "Create thumbnails for lightboxed images"

@ -67,6 +67,9 @@ ru:
rss_posts_in_topic: 'RSS лента темы ''%{topic}' rss_posts_in_topic: 'RSS лента темы ''%{topic}'
rss_topics_in_category: 'RSS лента тем в категории ''%{category}''' rss_topics_in_category: 'RSS лента тем в категории ''%{category}'''
author_wrote: '%{author} написал:' author_wrote: '%{author} написал:'
num_posts: 'Сообщений:'
num_participants: 'Участников:'
read_full_topic: 'Читать всю тему'
private_message_abbrev: ЛС private_message_abbrev: ЛС
rss_description: rss_description:
latest: 'Последние темы' latest: 'Последние темы'
@ -85,7 +88,7 @@ ru:
trust_level_5: trust_level_5 trust_level_5: trust_level_5
education: education:
until_posts: until_posts:
one: сообщение one: '1 сообщение'
other: '%{count} сообщений' other: '%{count} сообщений'
few: '%{count} сообщения' few: '%{count} сообщения'
many: '%{count} сообщений' many: '%{count} сообщений'
@ -510,7 +513,7 @@ ru:
delete_removed_posts_after: 'Количество часов, после которого сообщение, удаленное пользователем, удаляется.' delete_removed_posts_after: 'Количество часов, после которого сообщение, удаленное пользователем, удаляется.'
max_image_width: 'Максимальная ширина изображений, добавляемых в сообщение' max_image_width: 'Максимальная ширина изображений, добавляемых в сообщение'
max_image_height: 'Максимальная высота изображения в сообщении' max_image_height: 'Максимальная высота изображения в сообщении'
category_featured_topics: 'Количество отображаемых тем в категориях на странице /categories' category_featured_topics: 'Количество тем, отображаемых в одной категории или на странице категорий. После изменения значения требуется около 15 минут для обновления списков.'
add_rel_nofollow_to_user_content: 'Добавить "rel nofollow" для всех ссылок за исключением внутренних (включая родительский домен). Изменение данной настройки потребует обновления всех сообщений (<code>rake posts:rebake</code>)' add_rel_nofollow_to_user_content: 'Добавить "rel nofollow" для всех ссылок за исключением внутренних (включая родительский домен). Изменение данной настройки потребует обновления всех сообщений (<code>rake posts:rebake</code>)'
exclude_rel_nofollow_domains: 'Разделенный запятыми список доменов, в которых nofollow не добавлено (tld.com автоматически позволит также и sub.tld.com)' exclude_rel_nofollow_domains: 'Разделенный запятыми список доменов, в которых nofollow не добавлено (tld.com автоматически позволит также и sub.tld.com)'
post_excerpt_maxlength: 'Максимальное количество символов выдержки из сообщения' post_excerpt_maxlength: 'Максимальное количество символов выдержки из сообщения'
@ -602,6 +605,7 @@ ru:
suggested_topics: 'Количество рекомендованных тем, отображаемых внизу текущей темы' suggested_topics: 'Количество рекомендованных тем, отображаемых внизу текущей темы'
clean_up_uploads: 'Удалить неиспользуемые загрузки для предотвращения хранения нелегального контента. ВНИМАНИЕ: рекомендуется сделать резервную копию директории /uploads перед включением данной настройки.' clean_up_uploads: 'Удалить неиспользуемые загрузки для предотвращения хранения нелегального контента. ВНИМАНИЕ: рекомендуется сделать резервную копию директории /uploads перед включением данной настройки.'
clean_orphan_uploads_grace_period_hours: 'Период (в часах) после которого неопубликованные вложения удаляются.' clean_orphan_uploads_grace_period_hours: 'Период (в часах) после которого неопубликованные вложения удаляются.'
purge_deleted_uploads_grace_period_days: 'Период (в днях) после которого удаленные вложения очищаются.'
enable_s3_uploads: 'Размещать загруженные файлы на Amazon S3' enable_s3_uploads: 'Размещать загруженные файлы на Amazon S3'
s3_upload_bucket: 'Наименование Amazon S3 bucket в который будут загружаться файлы. ВНИМАНИЕ: имя должно быть в нижнем регистре (см. http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html)' s3_upload_bucket: 'Наименование Amazon S3 bucket в который будут загружаться файлы. ВНИМАНИЕ: имя должно быть в нижнем регистре (см. http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html)'
s3_access_key_id: 'Amazon S3 access key для загрузки и хранения изображений' s3_access_key_id: 'Amazon S3 access key для загрузки и хранения изображений'
@ -637,7 +641,7 @@ ru:
min_title_similar_length: 'Минимальная длина названия темы, при которой тема будет проверена на наличие похожих' min_title_similar_length: 'Минимальная длина названия темы, при которой тема будет проверена на наличие похожих'
min_body_similar_length: 'Минимальная длина тела сообщения, при которой оно будет проверено на наличие похожих тем' min_body_similar_length: 'Минимальная длина тела сообщения, при которой оно будет проверено на наличие похожих тем'
category_colors: 'Разделенный чертой (|) список дозволенных hexadecimal цветов для категорий' category_colors: 'Разделенный чертой (|) список дозволенных hexadecimal цветов для категорий'
enable_wide_category_list: 'Включить традиционный полноразмерный список категорий.' enable_wide_category_list: 'Использовать обычный полноразмерный список категорий. ВНИМАНИЕ: на текущий момент данная настройка ничего не делает и в скором времени будет удалена. Полноразмерный список является единственно возможным.'
max_image_size_kb: 'Максимальный размер изображений для загрузки пользователем в КБ – убедитесь, что вы так же настроили лимит в nginx (client_max_body_size) / apache или прокси.' max_image_size_kb: 'Максимальный размер изображений для загрузки пользователем в КБ – убедитесь, что вы так же настроили лимит в nginx (client_max_body_size) / apache или прокси.'
max_attachment_size_kb: 'Максимальный размер файлов для загрузки пользователем в кб – убедитесь, что вы настроили лимит также в nginx (client_max_body_size) / apache или proxy.' max_attachment_size_kb: 'Максимальный размер файлов для загрузки пользователем в кб – убедитесь, что вы настроили лимит также в nginx (client_max_body_size) / apache или proxy.'
authorized_extensions: 'Список расширений файлов, разрешенных к загрузке, разделенный вертикальной чертой (|)' authorized_extensions: 'Список расширений файлов, разрешенных к загрузке, разделенный вертикальной чертой (|)'
@ -675,6 +679,7 @@ ru:
enable_names: 'Разрешить пользователям отображать полные имена' enable_names: 'Разрешить пользователям отображать полные имена'
display_name_on_posts: 'Отображать полные имена пользователей в сообщениях' display_name_on_posts: 'Отображать полные имена пользователей в сообщениях'
invites_shown: 'Максимальное количество приглашений, отображаемых на странице пользователя' invites_shown: 'Максимальное количество приглашений, отображаемых на странице пользователя'
short_progress_text_threshold: 'После достижения указанного числа сообщений в теме, бар будет отображать только текущий номер сообщения. Если вы измените ширину бара, вы можете изменить это значение.'
notification_types: notification_types:
mentioned: '%{display_username} упомянул вас в %{link}' mentioned: '%{display_username} упомянул вас в %{link}'
liked: '%{display_username} понравилось ваше сообщение в теме %{link}' liked: '%{display_username} понравилось ваше сообщение в теме %{link}'
@ -711,12 +716,24 @@ ru:
archived_disabled: 'Эта тема разархивирована. Она более не заморожена, и может быть изменена.' archived_disabled: 'Эта тема разархивирована. Она более не заморожена, и может быть изменена.'
closed_enabled: 'Эта тема закрыта. В ней больше нельзя отвечать.' closed_enabled: 'Эта тема закрыта. В ней больше нельзя отвечать.'
closed_disabled: 'Эта тема открыта. В ней можно отвечать.' closed_disabled: 'Эта тема открыта. В ней можно отвечать.'
autoclosed_enabled: autoclosed_enabled_days:
zero: 'Эта тема была автоматически закрыта через 1 день. В ней больше нельзя отвечать.' zero: 'Эта тема была автоматически закрыта через 1 день. В ней больше нельзя отвечать.'
one: 'Эта тема была автоматически закрыта через 1 день. В ней больше нельзя отвечать.' one: 'Эта тема была автоматически закрыта через 1 день. В ней больше нельзя отвечать.'
other: 'Эта тема была автоматически закрыта через %{count} дней. В ней больше нельзя отвечать.' other: 'Эта тема была автоматически закрыта спустя %{count} дней. В ней больше нельзя отвечать.'
few: 'Эта тема была автоматически закрыта спустя %{count} дня. В ней больше нельзя отвечать.' few: 'Эта тема была автоматически закрыта спустя %{count} дня. В ней больше нельзя отвечать.'
many: 'Эта тема была автоматически закрыта спустя %{count} дней. В ней больше нельзя отвечать.' many: 'Эта тема была автоматически закрыта спустя %{count} дней. В ней больше нельзя отвечать.'
autoclosed_enabled_hours:
zero: 'Эта тема была автоматически закрыта через 1 час. В ней больше нельзя отвечать.'
one: 'Эта тема была автоматически закрыта через 1 час. В ней больше нельзя отвечать.'
other: 'Эта тема была автоматически закрыта спустя %{count} часов. В ней больше нельзя отвечать.'
few: 'Эта тема была автоматически закрыта спустя %{count} часа. В ней больше нельзя отвечать.'
many: 'Эта тема была автоматически закрыта спустя %{count} часов. В ней больше нельзя отвечать.'
autoclosed_enabled_minutes:
zero: 'Эта тема была автоматически закрыта через 1 минуту. В ней больше нельзя отвечать.'
one: 'Эта тема была автоматически закрыта через 1 минуту. В ней больше нельзя отвечать.'
other: 'Эта тема была автоматически закрыта спустя %{count} минут. В ней больше нельзя отвечать.'
few: 'Эта тема была автоматически закрыта спустя %{count} минуты. В ней больше нельзя отвечать.'
many: 'Эта тема была автоматически закрыта спустя %{count} минут. В ней больше нельзя отвечать.'
autoclosed_disabled: 'Эта тема открыта. В ней можно отвечать.' autoclosed_disabled: 'Эта тема открыта. В ней можно отвечать.'
pinned_enabled: 'Эта тема прилеплена. Она будет всегда отображаться первой в списке тем своей категории, пока не будет отлеплена модератором, или сброшена вниз, когда каждый пользователь нажмет кнопку «Отлепить»' pinned_enabled: 'Эта тема прилеплена. Она будет всегда отображаться первой в списке тем своей категории, пока не будет отлеплена модератором, или сброшена вниз, когда каждый пользователь нажмет кнопку «Отлепить»'
pinned_disabled: 'Эта тема отлеплена. Она больше не будет отображаться наверху списка тем категории.' pinned_disabled: 'Эта тема отлеплена. Она больше не будет отображаться наверху списка тем категории.'
@ -801,7 +818,7 @@ ru:
other: '%{count} пользователей ожидают подтверждения' other: '%{count} пользователей ожидают подтверждения'
few: '%{count} пользователя ожидают подтверждения' few: '%{count} пользователя ожидают подтверждения'
many: '%{count} пользователей ожидают подтверждения' many: '%{count} пользователей ожидают подтверждения'
text_body_template: "Новые пользователи ожидают подтверждения (или отказа).\n\n[Проверьте список в секции администрирования](/admin/users/list/pending).\n" text_body_template: "Новые пользователи ожидают подтверждения (или отказа) перед тем, как они получат доступ до форума.\n\n[Проверьте список в секции администрирования](%{base_url}/admin/users/list/pending).\n"
unsubscribe_link: 'Для того, чтобы отписаться от подобных сообщений, перейдите в [настройки профиля](%{user_preferences_url}).' unsubscribe_link: 'Для того, чтобы отписаться от подобных сообщений, перейдите в [настройки профиля](%{user_preferences_url}).'
user_notifications: user_notifications:
previous_discussion: 'Предыдущие ответы' previous_discussion: 'Предыдущие ответы'
@ -836,6 +853,11 @@ ru:
click_here: 'нажмите здесь' click_here: 'нажмите здесь'
from: 'Cводка новостей сайта %{site_name}' from: 'Cводка новостей сайта %{site_name}'
read_more: 'Читать еще' read_more: 'Читать еще'
posts:
one: '1 сообщение'
other: '%{count} сообщений'
few: '%{count} сообщения'
many: '%{count} сообщений'
forgot_password: forgot_password:
subject_template: '[%{site_name}] Сброс пароля' subject_template: '[%{site_name}] Сброс пароля'
text_body_template: "Кто-то запросил смену вашего пароля на сайте [%{site_name}](%{base_url}).\n\nЕсли это были не вы, спокойно проигнорируйте это письмо.\n\nПройдите по следующей ссылке, чтобы задать новый пароль:\n%{base_url}/users/password-reset/%{email_token}\n" text_body_template: "Кто-то запросил смену вашего пароля на сайте [%{site_name}](%{base_url}).\n\nЕсли это были не вы, спокойно проигнорируйте это письмо.\n\nПройдите по следующей ссылке, чтобы задать новый пароль:\n%{base_url}/users/password-reset/%{email_token}\n"

@ -140,7 +140,7 @@ Discourse::Application.routes.draw do
get 'user_preferences' => 'users#user_preferences_redirect' get 'user_preferences' => 'users#user_preferences_redirect'
get 'users/:username/private-messages' => 'user_actions#private_messages', constraints: {username: USERNAME_ROUTE_FORMAT} get 'users/:username/private-messages' => 'user_actions#private_messages', constraints: {username: USERNAME_ROUTE_FORMAT}
get 'users/:username/private-messages/:filter' => 'user_actions#private_messages', constraints: {username: USERNAME_ROUTE_FORMAT} get 'users/:username/private-messages/:filter' => 'user_actions#private_messages', constraints: {username: USERNAME_ROUTE_FORMAT}
get 'users/:username' => 'users#show', constraints: {username: USERNAME_ROUTE_FORMAT} get 'users/:username' => 'users#show', as: 'userpage', constraints: {username: USERNAME_ROUTE_FORMAT}
put 'users/:username' => 'users#update', constraints: {username: USERNAME_ROUTE_FORMAT} put 'users/:username' => 'users#update', constraints: {username: USERNAME_ROUTE_FORMAT}
get 'users/:username/preferences' => 'users#preferences', constraints: {username: USERNAME_ROUTE_FORMAT}, as: :email_preferences get 'users/:username/preferences' => 'users#preferences', constraints: {username: USERNAME_ROUTE_FORMAT}, as: :email_preferences
get 'users/:username/preferences/email' => 'users#preferences', constraints: {username: USERNAME_ROUTE_FORMAT} get 'users/:username/preferences/email' => 'users#preferences', constraints: {username: USERNAME_ROUTE_FORMAT}

@ -76,6 +76,9 @@ users:
must_approve_users: must_approve_users:
client: true client: true
default: false default: false
min_password_length:
client: true
default: 8
enable_google_logins: enable_google_logins:
client: true client: true
default: true default: true
@ -177,6 +180,7 @@ posting:
default: true default: true
post_undo_action_window_mins: 10 post_undo_action_window_mins: 10
max_mentions_per_post: 10 max_mentions_per_post: 10
newuser_max_replies_per_topic: 3
newuser_max_mentions_per_post: 2 newuser_max_mentions_per_post: 2
onebox_max_chars: 5000 onebox_max_chars: 5000
title_min_entropy: 10 title_min_entropy: 10

@ -7,6 +7,7 @@ class ComposerMessagesFinder
def find def find
check_education_message || check_education_message ||
check_new_user_many_replies ||
check_avatar_notification || check_avatar_notification ||
check_sequential_replies || check_sequential_replies ||
check_dominating_topic check_dominating_topic
@ -32,6 +33,12 @@ class ComposerMessagesFinder
nil nil
end end
# New users have a limited number of replies in a topic
def check_new_user_many_replies
return unless replying? && @user.posted_too_much_in_topic?(@details[:topic_id])
{templateName: 'composer/education', body: PrettyText.cook(I18n.t('education.too_many_replies', newuser_max_replies_per_topic: SiteSetting.newuser_max_replies_per_topic)) }
end
# Should a user be contacted to update their avatar? # Should a user be contacted to update their avatar?
def check_avatar_notification def check_avatar_notification

@ -0,0 +1,12 @@
class PasswordValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return unless record.password_required?
if value.nil?
record.errors.add(attribute, :blank)
elsif value.length < SiteSetting.min_password_length
record.errors.add(attribute, :too_short, count: SiteSetting.min_password_length)
end
end
end

@ -5,6 +5,7 @@ class Validators::PostValidator < ActiveModel::Validator
presence(record) presence(record)
stripped_length(record) stripped_length(record)
raw_quality(record) raw_quality(record)
max_posts_validator(record)
max_mention_validator(record) max_mention_validator(record)
max_images_validator(record) max_images_validator(record)
max_attachments_validator(record) max_attachments_validator(record)
@ -40,6 +41,12 @@ class Validators::PostValidator < ActiveModel::Validator
end end
end end
def max_posts_validator(post)
if post.acting_user.present? && post.acting_user.posted_too_much_in_topic?(post.topic_id)
post.errors.add(:base, I18n.t(:too_many_replies))
end
end
# Ensure new users can not put too many images in a post # Ensure new users can not put too many images in a post
def max_images_validator(post) def max_images_validator(post)
add_error_if_count_exceeded(post, :too_many_images, post.image_count, SiteSetting.newuser_max_images) unless acting_user_is_trusted?(post) add_error_if_count_exceeded(post, :too_many_images, post.image_count, SiteSetting.newuser_max_images) unless acting_user_is_trusted?(post)

@ -10,6 +10,7 @@ describe ComposerMessagesFinder do
it "calls all the message finders" do it "calls all the message finders" do
finder.expects(:check_education_message).once finder.expects(:check_education_message).once
finder.expects(:check_new_user_many_replies).once
finder.expects(:check_avatar_notification).once finder.expects(:check_avatar_notification).once
finder.expects(:check_sequential_replies).once finder.expects(:check_sequential_replies).once
finder.expects(:check_dominating_topic).once finder.expects(:check_dominating_topic).once
@ -56,6 +57,24 @@ describe ComposerMessagesFinder do
finder.check_education_message.should be_blank finder.check_education_message.should be_blank
end end
end end
end
context '.check_new_user_many_replies' do
let(:user) { Fabricate.build(:user) }
context 'replying' do
let(:finder) { ComposerMessagesFinder.new(user, composerAction: 'reply') }
it "has no message when `posted_too_much_in_topic?` is false" do
user.expects(:posted_too_much_in_topic?).returns(false)
finder.check_new_user_many_replies.should be_blank
end
it "has a message when a user has posted too much" do
user.expects(:posted_too_much_in_topic?).returns(true)
finder.check_new_user_many_replies.should be_present
end
end
end end

@ -0,0 +1,60 @@
require 'spec_helper'
describe PasswordValidator do
let(:validator) { described_class.new({attributes: :password}) }
subject(:validate) { validator.validate_each(record,:password,@password) }
context "password required" do
let(:record) { u = Fabricate.build(:user, password: @password); u.password_required!; u }
context "min password length is 8" do
before { SiteSetting.stubs(:min_password_length).returns(8) }
it "doesn't add an error when password is good" do
@password = "weron235alsfn234"
validate
record.errors[:password].should_not be_present
end
it "adds an error when password is too short" do
@password = "p"
validate
record.errors[:password].should be_present
end
it "adds an error when password is blank" do
@password = ''
validate
record.errors[:password].should be_present
end
it "adds an error when password is nil" do
@password = nil
validate
record.errors[:password].should be_present
end
end
context "min password length is 12" do
before { SiteSetting.stubs(:min_password_length).returns(12) }
it "adds an error when password length is 11" do
@password = "gt38sdt92bv"
validate
record.errors[:password].should be_present
end
end
end
context "password not required" do
let(:record) { Fabricate.build(:user, password: @password) }
it "doesn't add an error if password is not required" do
@password = nil
validate
record.errors[:password].should_not be_present
end
end
end

@ -24,6 +24,20 @@ describe Validators::PostValidator do
end end
end end
context "too_many_posts" do
it "should be invalid when the user has posted too much" do
post.user.expects(:posted_too_much_in_topic?).returns(true)
validator.max_posts_validator(post)
expect(post.errors.count).to be > 0
end
it "should be valid when the user hasn't posted too much" do
post.user.expects(:posted_too_much_in_topic?).returns(false)
validator.max_posts_validator(post)
expect(post.errors.count).to be(0)
end
end
context "invalid post" do context "invalid post" do
it "should be invalid" do it "should be invalid" do
validator.validate(post) validator.validate(post)

@ -87,6 +87,14 @@ describe Topic do
Then { scheduled_jobs_for(:close_topic).should have(2).jobs } Then { scheduled_jobs_for(:close_topic).should have(2).jobs }
end end
end end
context 'a topic that has been auto-closed' do
Given(:admin) { Fabricate(:admin) }
Given!(:auto_closed_topic) { Fabricate(:topic, user: admin, closed: true, auto_close_at: 1.day.ago, auto_close_user_id: admin.id, auto_close_started_at: 6.days.ago) }
When { auto_closed_topic.update_status('closed', false, admin) }
Then { auto_closed_topic.reload.auto_close_at.should be_nil }
And { auto_closed_topic.auto_close_started_at.should be_nil }
end
end end
end end
end end

@ -1,3 +1,3 @@
/*jshint maxlen:10000000 */ /*jshint maxlen:10000000 */
Discourse.SiteSettingsOriginal = {"title":"Discourse Meta","logo_url":"/assets/logo.png","logo_small_url":"/assets/logo-single.png","traditional_markdown_linebreaks":false,"top_menu":"latest|new|unread|read|favorited|categories","post_menu":"like|edit|flag|delete|share|bookmark|reply","share_links":"twitter|facebook|google+|email","track_external_right_clicks":false,"must_approve_users":false,"ga_tracking_code":"UA-33736483-2","ga_domain_name":"","enable_long_polling":true,"polling_interval":3000,"anon_polling_interval":30000,"min_post_length":20,"max_post_length":16000,"min_topic_title_length":15,"max_topic_title_length":255,"min_private_message_title_length":2,"allow_uncategorized_topics":true,"min_search_term_length":3,"flush_timings_secs":5,"suppress_reply_directly_below":true,"email_domains_blacklist":"mailinator.com","email_domains_whitelist":null,"version_checks":true,"min_title_similar_length":10,"min_body_similar_length":15,"category_colors":"BF1E2E|F1592A|F7941D|9EB83B|3AB54A|12A89D|25AAE2|0E76BD|652D90|92278F|ED207B|8C6238|231F20|808281|B3B5B4|283890","max_upload_size_kb":1024,"category_featured_topics":6,"favicon_url":"/assets/favicon.ico","dynamic_favicon":false,"uncategorized_name":"uncategorized","uncategorized_color":"AB9364","uncategorized_text_color":"FFFFFF","invite_only":false,"login_required":false,"enable_local_logins":true,"enable_local_account_create":true,"enable_google_logins":true,"enable_yahoo_logins":true,"enable_twitter_logins":true,"enable_facebook_logins":true,"enable_cas_logins":false,"enable_github_logins":true,"enable_persona_logins":true,"educate_until_posts":2,"topic_views_heat_low":1000,"topic_views_heat_medium":2000,"topic_views_heat_high":5000,"min_private_message_post_length":5,"faq_url":"","tos_url":"","privacy_policy_url":"","authorized_extensions":".jpg|.jpeg|.png|.gif|.txt","relative_date_duration":14,"delete_removed_posts_after":24,"delete_user_max_age":7, "default_code_lang": "lang-auto"}; Discourse.SiteSettingsOriginal = {"title":"Discourse Meta","logo_url":"/assets/logo.png","logo_small_url":"/assets/logo-single.png","traditional_markdown_linebreaks":false,"top_menu":"latest|new|unread|read|favorited|categories","post_menu":"like|edit|flag|delete|share|bookmark|reply","share_links":"twitter|facebook|google+|email","track_external_right_clicks":false,"must_approve_users":false,"ga_tracking_code":"UA-33736483-2","ga_domain_name":"","enable_long_polling":true,"polling_interval":3000,"anon_polling_interval":30000,"min_post_length":20,"max_post_length":16000,"min_topic_title_length":15,"max_topic_title_length":255,"min_private_message_title_length":2,"allow_uncategorized_topics":true,"min_search_term_length":3,"flush_timings_secs":5,"suppress_reply_directly_below":true,"email_domains_blacklist":"mailinator.com","email_domains_whitelist":null,"version_checks":true,"min_title_similar_length":10,"min_body_similar_length":15,"category_colors":"BF1E2E|F1592A|F7941D|9EB83B|3AB54A|12A89D|25AAE2|0E76BD|652D90|92278F|ED207B|8C6238|231F20|808281|B3B5B4|283890","max_upload_size_kb":1024,"category_featured_topics":6,"favicon_url":"/assets/favicon.ico","dynamic_favicon":false,"uncategorized_name":"uncategorized","uncategorized_color":"AB9364","uncategorized_text_color":"FFFFFF","invite_only":false,"login_required":false,"min_password_length":8,"enable_local_logins":true,"enable_local_account_create":true,"enable_google_logins":true,"enable_yahoo_logins":true,"enable_twitter_logins":true,"enable_facebook_logins":true,"enable_cas_logins":false,"enable_github_logins":true,"enable_persona_logins":true,"educate_until_posts":2,"topic_views_heat_low":1000,"topic_views_heat_medium":2000,"topic_views_heat_high":5000,"min_private_message_post_length":5,"faq_url":"","tos_url":"","privacy_policy_url":"","authorized_extensions":".jpg|.jpeg|.png|.gif|.txt","relative_date_duration":14,"delete_removed_posts_after":24,"delete_user_max_age":7, "default_code_lang": "lang-auto"};
Discourse.SiteSettings = jQuery.extend(true, {}, Discourse.SiteSettingsOriginal); Discourse.SiteSettings = jQuery.extend(true, {}, Discourse.SiteSettingsOriginal);