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

View File

@ -31,7 +31,7 @@ Discourse.CreateAccountController = Discourse.Controller.extend(Discourse.ModalF
}.property('authOptions.auth_provider'),
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(),
// Validate the name
@ -273,7 +273,7 @@ Discourse.CreateAccountController = Discourse.Controller.extend(Discourse.ModalF
}
// If too short
if (password.length < 6) {
if (password.length < Discourse.SiteSettings.min_password_length) {
return Discourse.InputValidation.create({
failed: true,
reason: I18n.t('user.password.too_short')

View File

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

View File

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

View File

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

View File

@ -41,17 +41,11 @@
}
}
@mixin portrait {
@media only screen and (max-width : 320px) {
@content;
}
}
@mixin landscape {
@media only screen and (min-width : 321px) {
@content;
}
}
@mixin mobile-portrait { @media only screen and (max-width : 320px) { @content; } }
@mixin not-mobile-portrait { @media only screen and (min-width : 321px) { @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; } }
// CSS3 properties
// --------------------------------------------------

View File

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

View File

@ -43,7 +43,10 @@ a.loading-onebox {
.onebox-result-body {
padding-top: 5px;
img {
max-width:200px;
max-width: 100px;
max-height: 80%;
float: left;
margin-right: 10px;
}
h3, h4 {
margin: 0px !important;
@ -55,12 +58,6 @@ a.loading-onebox {
code {
max-height: 400px;
}
img {
max-width: 30%;
max-height: 80%;
float: left;
margin-right: 10px;
}
.metrics {
clear: both;
padding-bottom: 25px;

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
tbody tr {
> tbody > tr {
background-color: $white;
&:nth-child(even) {
background-color: darken($white, 4%);
@ -163,19 +163,27 @@
// --------------------------------------------------
#topic-list.categories {
.badge-category {
display: inline-block;
margin-top: 1px;
}
.category-description {
margin-top: 8px;
}
.featured-users {
@include portrait {
@include mobile-portrait {
margin-bottom: 10px;
padding-top: 8px;
clear: left;
}
@include landscape {
@include not-mobile-portrait {
float: right;
}
}
.latest {
@include portrait { width: 150px; }
@include landscape { width: 270px; }
@include mobile-portrait { width: 150px; }
@include mobile-landscape { width: 270px; }
@include tablet-landscape { width: 450px; }
.featured-topic {
margin: 8px 0;
a.last-posted-at, a.last-posted-at:visited {
@ -184,7 +192,22 @@
}
}
.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; }
}
}
}

View File

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

View File

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

View File

@ -22,7 +22,7 @@ TopicStatusUpdate = Struct.new(:topic, :user) do
topic.update_column status.name, status.enabled?
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
end

View File

@ -143,6 +143,7 @@ class User < ActiveRecord::Base
where(username_lower: username.downcase).first
end
def enqueue_welcome_message(message_type)
return unless SiteSetting.send_welcome_message?
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?
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
def password_required!
@password_required = true
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)
return false unless password_hash && salt
self.password_hash == hash_password(password, salt)
@ -340,6 +353,10 @@ class User < ActiveRecord::Base
topics_allowed.where(archetype: Archetype.private_message).count
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
excerpt = PrettyText.excerpt(bio_cooked, 350)
return excerpt if excerpt.blank? || has_trust_level?(:basic)
@ -556,12 +573,6 @@ class User < ActiveRecord::Base
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
Jobs.enqueue(:user_email,
type: :signup_after_approval,

View File

@ -2,22 +2,21 @@
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<% lang = SiteSetting.find_by_name('default_locale').try(:value) %>
<% site_email = SiteSetting.find_by_name('contact_email').try(:value) %>
<title><%= @title %></title>
<link><%= @link %></link>
<description><%= @description %></description>
<% if lang %>
<language><%= lang %></language>
<% end %>
<%= "<language>#{lang}</language>" if lang %>
<lastBuildDate><%= @topic_list.topics.first.created_at.rfc2822 %></lastBuildDate>
<atom:link href="<%= @atom_link %>" rel="self" type="application/rss+xml" />
<% @topic_list.topics.each do |topic| %>
<% topic_url = Discourse.base_url + topic.relative_url -%>
<item>
<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>
<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>
<%= topic.posts.first.cooked.html_safe %>
</blockquote>

View File

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

View File

@ -197,7 +197,7 @@ ru:
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_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_you: 'Размещено <a href=''{{userUrl}}''>Вами</a>'
sent_by_user: 'Отправлено пользователем <a href=''{{userUrl}}''>{{user}}</a>'
@ -225,6 +225,8 @@ ru:
latest_by: 'последние по'
toggle_ordering: 'изменить сортировку'
subcategories: 'Подкатегории:'
total_topics: 'Всего тем: %{count}'
total_posts: 'Всего сообщений: %{count}'
user:
said: '{{username}} писал(а):'
profile: Профайл
@ -393,12 +395,14 @@ ru:
month_desc: 'создано тем за последние 30 дней'
week: неделя
week_desc: 'создано тем за последние 7 дней'
day: день
first_post: 'Первое сообщение'
mute: Отключить
unmute: Включить
summary:
enabled_description: 'Вы просматриваете только популярные сообщения в данной теме. Для просмотра всех сообщений нажмите кнопку ниже.'
description: 'В теме <b>{{count}}</b> сообщений. Хотите посмотреть только сообщения релевантные теме?'
description_time: 'В теме <b>{{count}}</b> сообщений со средним временем чтения <b>{{readingTime}} минут</b>. Сократить время чтения, отобразив только важные сообщения?'
enable: 'Сводка по теме'
disable: 'Показать все сообщения'
private_message_info:
@ -520,8 +524,10 @@ ru:
help: 'Справка по Markdown'
toggler: 'скрыть / показать панель редактирования'
admin_options_title: 'Дополнительные настройки темы'
auto_close_label: 'Автоматически закрыть тему после:'
auto_close_units: дней
auto_close_label: 'Автоматически закрыть тему:'
auto_close_units: '(# часов, время, или штамп времени)'
auto_close_examples: 'Например: 24, 17:00, 2013-11-22 14:00'
auto_close_error: 'Пожалуйста, введите правильное значение'
notifications:
title: 'уведомления об упоминании @name в сообщениях, ответах на ваши сообщения и темы, личные сообщения и т.д.'
none: 'На данный момент уведомлений нет.'
@ -534,8 +540,8 @@ ru:
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}}'
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}} принял ваше приглашение'
moved_post: '<i title=''moved post'' class=''fa fa-arrow-right''></i> {{username}} переместил сообщение в {{link}}'
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}}'
total_flagged: 'всего сообщений с жалобами'
upload_selector:
title: 'Add an image'
@ -653,8 +659,10 @@ ru:
title: 'текущее местоположение в теме'
jump_top: 'перейти к первому сообщению'
jump_bottom: 'перейти к последнему сообщению'
jump_bottom_with_number: 'перейти к сообщению %{post_number}'
total: 'всего сообщений'
current: 'текущее сообщение'
position: '%{current} сообщение из %{total}'
notifications:
title: '&nbsp;'
reasons:
@ -778,6 +786,11 @@ ru:
many: '(сообщение отозвано автором и будет автоматически удалено через %{count} часов при отсутствии жалоб)'
deleted_by: 'Удалено'
expand_collapse: развернуть/свернуть
gap:
one: '1 сообщение пропущено'
other: '{{count}} сообщений пропущено'
few: '{{count}} сообщения пропущено'
many: '{{count}} сообщений пропущено'
has_replies:
one: ответ
other: ответов
@ -949,6 +962,25 @@ ru:
other: 'Вы уверены, что хотите удалить эти сообщения?'
few: 'Вы уверены, что хотите удалить сообщения?'
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:
can: 'может&hellip; '
none: '(без категории)'
@ -981,6 +1013,7 @@ ru:
already_used: 'Цвет уже используется другой категорией'
security: Безопасность
auto_close_label: 'Закрыть тему через:'
auto_close_units: часов
edit_permissions: 'Изменить права доступа'
add_permission: 'Добавить права'
this_year: 'в год'
@ -1135,6 +1168,7 @@ ru:
disagree_title: 'Удалить все жалобы с данного сообщения'
delete_spammer_title: 'Удалить пользователя и все его сообщения.'
flagged_by: 'Отмечено'
system: Системные
error: 'что-то пошло не так'
view_message: Ответить
no_results: 'Жалоб нет.'
@ -1430,3 +1464,5 @@ ru:
rate_limits: 'Ограничения'
developer: Разработчик
uncategorized: Без категории
lightbox:
download: загрузить

View File

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

View File

@ -27,6 +27,8 @@ en:
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."
too_many_replies: "Sorry you can't reply any more times in that topic."
too_many_mentions:
zero: "Sorry, you can't mention other users."
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?
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:
attributes:
category:
@ -594,6 +603,7 @@ en:
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_account_create: "Enable creating new local accounts"
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_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_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"
create_thumbnails: "Create thumbnails for lightboxed images"

View File

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

View File

@ -140,7 +140,7 @@ Discourse::Application.routes.draw do
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/: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}
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}

View File

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

View File

@ -7,6 +7,7 @@ class ComposerMessagesFinder
def find
check_education_message ||
check_new_user_many_replies ||
check_avatar_notification ||
check_sequential_replies ||
check_dominating_topic
@ -32,6 +33,12 @@ class ComposerMessagesFinder
nil
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?
def check_avatar_notification

View File

@ -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

View File

@ -5,6 +5,7 @@ class Validators::PostValidator < ActiveModel::Validator
presence(record)
stripped_length(record)
raw_quality(record)
max_posts_validator(record)
max_mention_validator(record)
max_images_validator(record)
max_attachments_validator(record)
@ -40,6 +41,12 @@ class Validators::PostValidator < ActiveModel::Validator
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
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)

View File

@ -10,6 +10,7 @@ describe ComposerMessagesFinder do
it "calls all the message finders" do
finder.expects(:check_education_message).once
finder.expects(:check_new_user_many_replies).once
finder.expects(:check_avatar_notification).once
finder.expects(:check_sequential_replies).once
finder.expects(:check_dominating_topic).once
@ -56,6 +57,24 @@ describe ComposerMessagesFinder do
finder.check_education_message.should be_blank
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

View File

@ -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

View File

@ -24,6 +24,20 @@ describe Validators::PostValidator do
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
it "should be invalid" do
validator.validate(post)

View File

@ -87,6 +87,14 @@ describe Topic do
Then { scheduled_jobs_for(:close_topic).should have(2).jobs }
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

View File

@ -1,3 +1,3 @@
/*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);