Merge branch 'master' of github.com:discourse/discourse into bbpress-missing-display-name

This commit is contained in:
Jay Pfaffman 2017-01-11 11:28:38 -08:00
commit e307bbccf9
38 changed files with 545 additions and 129 deletions

View File

@ -15,7 +15,7 @@ To learn more about the philosophy and goals of the project, [visit **discourse.
<a href="http://discuss.howtogeek.com"><img src="https://www.discourse.org/faq/17/how-to-geek-discourse.png" width="720px"></a> <a href="http://discuss.howtogeek.com"><img src="https://www.discourse.org/faq/17/how-to-geek-discourse.png" width="720px"></a>
<a href="https://talk.turtlerockstudios.com/"><img src="https://www.discourse.org/faq/17/turtle-rock-discourse.png" width="720px"></a> <a href="https://talk.turtlerockstudios.com/"><img src="https://www.discourse.org/faq/17/turtle-rock-discourse.png" width="720px"></a>
<a href="https://discuss.atom.io"><img src="https://www.discourse.org/faq/15/nexus-7-2013-mobile-discourse.png?v=2" alt="Atom" width="430px"></a> &nbsp; <a href="https://discuss.atom.io"><img src="https://www.discourse.org/faq/17/nexus-7-2013-mobile-discourse.png" alt="Atom" width="430px"></a> &nbsp;
<a href="//discourse.soylent.com"><img src="https://www.discourse.org/faq/15/iphone-5s-mobile-discourse.png" alt="Soylent" width="270px"></a> <a href="//discourse.soylent.com"><img src="https://www.discourse.org/faq/15/iphone-5s-mobile-discourse.png" alt="Soylent" width="270px"></a>
Browse [lots more notable Discourse instances](http://www.discourse.org/faq/customers/). Browse [lots more notable Discourse instances](http://www.discourse.org/faq/customers/).

View File

@ -6,6 +6,10 @@ export default Em.Component.extend(UploadMixin, {
tagName: "span", tagName: "span",
uploadUrl: "/invites/upload_csv", uploadUrl: "/invites/upload_csv",
validateUploadedFilesOptions() {
return { csvOnly: true };
},
@computed("uploading") @computed("uploading")
uploadButtonText(uploading) { uploadButtonText(uploading) {
return uploading ? I18n.t("uploading") : I18n.t("user.invited.bulk_invite.text"); return uploading ? I18n.t("uploading") : I18n.t("user.invited.bulk_invite.text");

View File

@ -192,6 +192,11 @@ export function validateUploadedFile(file, opts) {
bootbox.alert(I18n.t('post.errors.upload_not_authorized', { authorized_extensions: authorizedImagesExtensions() })); bootbox.alert(I18n.t('post.errors.upload_not_authorized', { authorized_extensions: authorizedImagesExtensions() }));
return false; return false;
} }
} else if (opts["csvOnly"]) {
if (!(/\.csv$/i).test(name)) {
bootbox.alert(I18n.t('user.invited.bulk_invite.error'));
return false;
}
} else { } else {
if (!authorizesAllExtensions() && !isAuthorizedFile(name)) { if (!authorizesAllExtensions() && !isAuthorizedFile(name)) {
bootbox.alert(I18n.t('post.errors.upload_not_authorized', { authorized_extensions: authorizedExtensions() })); bootbox.alert(I18n.t('post.errors.upload_not_authorized', { authorized_extensions: authorizedExtensions() }));

View File

@ -1,9 +1,18 @@
import { register } from 'pretty-text/engines/discourse-markdown/bbcode'; import { register } from 'pretty-text/engines/discourse-markdown/bbcode';
import { registerOption } from 'pretty-text/pretty-text';
import { performEmojiUnescape } from 'pretty-text/emoji';
registerOption((siteSettings, opts) => {
opts.enableEmoji = siteSettings.enable_emoji;
opts.emojiSet = siteSettings.emoji_set;
});
export function setup(helper) { export function setup(helper) {
register(helper, 'quote', {noWrap: true, singlePara: true}, (contents, bbParams, options) => { register(helper, 'quote', {noWrap: true, singlePara: true}, (contents, bbParams, options) => {
const params = {'class': 'quote'}; const params = {'class': 'quote'};
let username = null; let username = null;
const opts = helper.getOptions();
if (bbParams) { if (bbParams) {
const paramsSplit = bbParams.split(/\,\s*/); const paramsSplit = bbParams.split(/\,\s*/);
@ -52,7 +61,16 @@ export function setup(helper) {
if (postNumber > 0) { href += "/" + postNumber; } if (postNumber > 0) { href += "/" + postNumber; }
// get rid of username said stuff // get rid of username said stuff
header.pop(); header.pop();
header.push(['a', {'href': href}, topicInfo.title]);
let title = topicInfo.title;
if (opts.enableEmoji) {
title = performEmojiUnescape(topicInfo.title, {
getURL: opts.getURL, emojiSet: opts.emojiSet
});
}
header.push(['a', {'href': href}, title]);
} }
} }

View File

@ -85,6 +85,7 @@ fieldset {
pre code { pre code {
overflow: auto; overflow: auto;
tab-size: 4;
} }
// TODO figure out a clean place to put stuff like this // TODO figure out a clean place to put stuff like this

View File

@ -95,7 +95,14 @@ class Admin::BackupsController < Admin::AdminController
def readonly def readonly
enable = params.fetch(:enable).to_s == "true" enable = params.fetch(:enable).to_s == "true"
enable ? Discourse.enable_readonly_mode(user_enabled: true) : Discourse.disable_readonly_mode(user_enabled: true) readonly_mode_key = Discourse::USER_READONLY_MODE_KEY
if enable
Discourse.enable_readonly_mode(readonly_mode_key)
else
Discourse.disable_readonly_mode(readonly_mode_key)
end
render nothing: true render nothing: true
end end

View File

@ -46,6 +46,7 @@ class Admin::UsersController < Admin::AdminController
def delete_all_posts def delete_all_posts
@user = User.find_by(id: params[:user_id]) @user = User.find_by(id: params[:user_id])
@user.delete_all_posts!(guardian) @user.delete_all_posts!(guardian)
# staff action logs will have an entry for each post
render nothing: true render nothing: true
end end
@ -182,6 +183,8 @@ class Admin::UsersController < Admin::AdminController
@user.trust_level_locked = new_lock == "true" @user.trust_level_locked = new_lock == "true"
@user.save @user.save
StaffActionLogger.new(current_user).log_lock_trust_level(@user)
unless @user.trust_level_locked unless @user.trust_level_locked
p = Promotion.new(@user) p = Promotion.new(@user)
2.times{ p.review } 2.times{ p.review }
@ -210,12 +213,14 @@ class Admin::UsersController < Admin::AdminController
def activate def activate
guardian.ensure_can_activate!(@user) guardian.ensure_can_activate!(@user)
@user.activate @user.activate
StaffActionLogger.new(current_user).log_user_activate(@user, I18n.t('user.activated_by_staff'))
render json: success_json render json: success_json
end end
def deactivate def deactivate
guardian.ensure_can_deactivate!(@user) guardian.ensure_can_deactivate!(@user)
@user.deactivate @user.deactivate
StaffActionLogger.new(current_user).log_user_deactivate(@user, I18n.t('user.deactivated_by_staff'))
refresh_browser @user refresh_browser @user
render nothing: true render nothing: true
end end

View File

@ -156,9 +156,9 @@ class InvitesController < ApplicationController
Scheduler::Defer.later("Upload CSV") do Scheduler::Defer.later("Upload CSV") do
begin begin
data = if extension == ".csv" data = if extension.downcase == ".csv"
path = Invite.create_csv(file, name) path = Invite.create_csv(file, name)
Jobs.enqueue(:bulk_invite, filename: "#{name}.csv", current_user_id: current_user.id) Jobs.enqueue(:bulk_invite, filename: "#{name}#{extension}", current_user_id: current_user.id)
{url: path} {url: path}
else else
failed_json.merge(errors: [I18n.t("bulk_invite.file_should_be_csv")]) failed_json.merge(errors: [I18n.t("bulk_invite.file_should_be_csv")])

View File

@ -17,7 +17,7 @@ class MetadataController < ApplicationController
name: SiteSetting.title, name: SiteSetting.title,
short_name: SiteSetting.title, short_name: SiteSetting.title,
display: 'standalone', display: 'standalone',
orientation: 'portrait', orientation: 'any',
start_url: "#{Discourse.base_uri}/", start_url: "#{Discourse.base_uri}/",
background_color: "##{ColorScheme.hex_for_name('secondary')}", background_color: "##{ColorScheme.hex_for_name('secondary')}",
theme_color: "##{ColorScheme.hex_for_name('header_background')}", theme_color: "##{ColorScheme.hex_for_name('header_background')}",

View File

@ -3,6 +3,7 @@ require_dependency 'email/message_builder'
require_dependency 'age_words' require_dependency 'age_words'
class UserNotifications < ActionMailer::Base class UserNotifications < ActionMailer::Base
include UserNotificationsHelper
helper :application helper :application
default charset: 'UTF-8' default charset: 'UTF-8'
@ -106,20 +107,27 @@ class UserNotifications < ActionMailer::Base
end end
@popular_topics = topics_for_digest[0,SiteSetting.digest_topics] @popular_topics = topics_for_digest[0,SiteSetting.digest_topics]
@other_new_for_you = topics_for_digest.size > SiteSetting.digest_topics ? topics_for_digest[SiteSetting.digest_topics..-1] : []
@popular_posts = if SiteSetting.digest_posts > 0
Post.order("posts.score DESC")
.for_mailing_list(user, min_date)
.where('posts.post_type = ?', Post.types[:regular])
.where('posts.deleted_at IS NULL AND posts.hidden = false AND posts.user_deleted = false')
.where("posts.post_number > ? AND posts.score > ?", 1, ScoreCalculator.default_score_weights[:like_score] * 5.0)
.limit(SiteSetting.digest_posts)
else
[]
end
if @popular_topics.present? if @popular_topics.present?
@other_new_for_you = topics_for_digest.size > SiteSetting.digest_topics ? topics_for_digest[SiteSetting.digest_topics..-1] : []
@popular_posts = if SiteSetting.digest_posts > 0
Post.order("posts.score DESC")
.for_mailing_list(user, min_date)
.where('posts.post_type = ?', Post.types[:regular])
.where('posts.deleted_at IS NULL AND posts.hidden = false AND posts.user_deleted = false')
.where("posts.post_number > ? AND posts.score > ?", 1, ScoreCalculator.default_score_weights[:like_score] * 5.0)
.limit(SiteSetting.digest_posts)
else
[]
end
@excerpts = {}
@popular_topics.map do |t|
@excerpts[t.first_post.id] = email_excerpt(t.first_post.cooked) if t.first_post.present?
end
# Try to find 3 interesting stats for the top of the digest # Try to find 3 interesting stats for the top of the digest
new_topics_count = Topic.new_since_last_seen(user, min_date).count new_topics_count = Topic.new_since_last_seen(user, min_date).count

View File

@ -1,3 +1,5 @@
require_dependency 'slug'
class Badge < ActiveRecord::Base class Badge < ActiveRecord::Base
# NOTE: These badge ids are not in order! They are grouped logically. # NOTE: These badge ids are not in order! They are grouped logically.
# When picking an id, *search* for it. # When picking an id, *search* for it.
@ -119,6 +121,10 @@ class Badge < ActiveRecord::Base
} }
end end
def awarded_for_trust_level?
id <= 4
end
def reset_grant_count! def reset_grant_count!
self.grant_count = UserBadge.where(badge_id: id).count self.grant_count = UserBadge.where(badge_id: id).count
save! save!
@ -208,6 +214,7 @@ SQL
def i18n_name def i18n_name
self.name.downcase.tr(' ', '_') self.name.downcase.tr(' ', '_')
end end
end end
# == Schema Information # == Schema Information

View File

@ -145,14 +145,14 @@ class GlobalSetting
attr_accessor :provider attr_accessor :provider
end end
def self.configure!
if Rails.env == "test" if Rails.env == "test"
@provider = BlankProvider.new @provider = BlankProvider.new
else else
@provider = @provider =
FileProvider.from(File.expand_path('../../../config/discourse.conf', __FILE__)) || FileProvider.from(File.expand_path('../../../config/discourse.conf', __FILE__)) ||
EnvProvider.new EnvProvider.new
end
end end
load_defaults
end end

View File

@ -55,7 +55,10 @@ class UserHistory < ActiveRecord::Base
rate_limited_like: 37, # not used anymore rate_limited_like: 37, # not used anymore
revoke_email: 38, revoke_email: 38,
deactivate_user: 39, deactivate_user: 39,
wizard_step: 40 wizard_step: 40,
lock_trust_level: 41,
unlock_trust_level: 42,
activate_user: 43
) )
end end
@ -91,7 +94,10 @@ class UserHistory < ActiveRecord::Base
:revoke_moderation, :revoke_moderation,
:backup_operation, :backup_operation,
:revoke_email, :revoke_email,
:deactivate_user] :deactivate_user,
:lock_trust_level,
:unlock_trust_level,
:activate_user]
end end
def self.staff_action_ids def self.staff_action_ids

View File

@ -274,7 +274,7 @@ class BadgeGranter
/*where*/ /*where*/
RETURNING id, user_id, granted_at RETURNING id, user_id, granted_at
) )
select w.*, username, locale FROM w select w.*, username, locale, (u.admin OR u.moderator) AS staff FROM w
JOIN users u on u.id = w.user_id JOIN users u on u.id = w.user_id
" "
@ -315,6 +315,8 @@ class BadgeGranter
# Make this variable in this scope # Make this variable in this scope
notification = nil notification = nil
next if (row.staff && badge.awarded_for_trust_level?)
I18n.with_locale(notification_locale) do I18n.with_locale(notification_locale) do
notification = Notification.create!( notification = Notification.create!(
user_id: row.user_id, user_id: row.user_id,

View File

@ -14,7 +14,6 @@ class StaffActionLogger
raise Discourse::InvalidParameters.new(:deleted_user) unless deleted_user && deleted_user.is_a?(User) raise Discourse::InvalidParameters.new(:deleted_user) unless deleted_user && deleted_user.is_a?(User)
UserHistory.create( params(opts).merge({ UserHistory.create( params(opts).merge({
action: UserHistory.actions[:delete_user], action: UserHistory.actions[:delete_user],
email: deleted_user.email,
ip_address: deleted_user.ip_address.to_s, ip_address: deleted_user.ip_address.to_s,
details: [:id, :username, :name, :created_at, :trust_level, :last_seen_at, :last_emailed_at].map { |x| "#{x}: #{deleted_user.send(x)}" }.join("\n") details: [:id, :username, :name, :created_at, :trust_level, :last_seen_at, :last_emailed_at].map { |x| "#{x}: #{deleted_user.send(x)}" }.join("\n")
})) }))
@ -96,6 +95,14 @@ class StaffActionLogger
})) }))
end end
def log_lock_trust_level(user, opts={})
raise Discourse::InvalidParameters.new(:user) unless user && user.is_a?(User)
UserHistory.create!( params(opts).merge({
action: UserHistory.actions[user.trust_level_locked ? :lock_trust_level : :unlock_trust_level],
target_user_id: user.id
}))
end
def log_site_setting_change(setting_name, previous_value, new_value, opts={}) def log_site_setting_change(setting_name, previous_value, new_value, opts={})
raise Discourse::InvalidParameters.new(:setting_name) unless setting_name.present? && SiteSetting.respond_to?(setting_name) raise Discourse::InvalidParameters.new(:setting_name) unless setting_name.present? && SiteSetting.respond_to?(setting_name)
UserHistory.create( params(opts).merge({ UserHistory.create( params(opts).merge({
@ -353,6 +360,15 @@ class StaffActionLogger
})) }))
end end
def log_user_activate(user, reason, opts={})
raise Discourse::InvalidParameters.new(:user) unless user
UserHistory.create(params(opts).merge({
action: UserHistory.actions[:activate_user],
target_user_id: user.id,
details: reason
}))
end
def log_wizard_step(step, opts={}) def log_wizard_step(step, opts={})
raise Discourse::InvalidParameters.new(:step) unless step raise Discourse::InvalidParameters.new(:step) unless step
UserHistory.create(params(opts).merge({ UserHistory.create(params(opts).merge({

View File

@ -17,8 +17,11 @@ class UserBlocker
unless @user.blocked? unless @user.blocked?
@user.blocked = true @user.blocked = true
if @user.save if @user.save
SystemMessage.create(@user, @opts[:message] || :blocked_by_staff) message_type = @opts[:message] || :blocked_by_staff
StaffActionLogger.new(@by_user).log_block_user(@user) if @by_user post = SystemMessage.create(@user, message_type)
if post && @by_user
StaffActionLogger.new(@by_user).log_block_user(@user, {context: "#{message_type}: '#{post.topic&.title rescue ''}'"})
end
end end
else else
false false

View File

@ -149,7 +149,7 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
<p style="color:inherit;font-size:14px;font-weight:400;line-height:1.3;margin:0 0 8px 0;padding:0;word-wrap:normal;"><%= t.user.username -%></p> <p style="color:inherit;font-size:14px;font-weight:400;line-height:1.3;margin:0 0 8px 0;padding:0;word-wrap:normal;"><%= t.user.username -%></p>
<% end %> <% end %>
</td> </td>
<%- if show_image_with_url(t.image_url) && t.featured_link.nil? -%> <%- if show_image_with_url(t.image_url) && t.featured_link.nil? && !(@excerpts[t.first_post&.id]||"").include?(t.image_url) -%>
<td style="margin:0;padding:0 16px 0 8px;text-align:right;" align="right"> <td style="margin:0;padding:0 16px 0 8px;text-align:right;" align="right">
<img src="<%= url_for_email(t.image_url) -%>" height="64" style="margin:auto;max-height:64px;max-width:100%;outline:0;text-align:right;text-decoration:none;"> <img src="<%= url_for_email(t.image_url) -%>" height="64" style="margin:auto;max-height:64px;max-width:100%;outline:0;text-align:right;text-decoration:none;">
</td> </td>
@ -163,7 +163,7 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
<tbody> <tbody>
<tr> <tr>
<td class="post-excerpt" style="color:#0a0a0a;font-size:14px;padding:0 16px 0 16px;text-align:left;width:100%;font-weight:normal;"> <td class="post-excerpt" style="color:#0a0a0a;font-size:14px;padding:0 16px 0 16px;text-align:left;width:100%;font-weight:normal;">
<%= email_excerpt(t.first_post.cooked) %> <%= @excerpts[t.first_post.id] %>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -6,8 +6,15 @@ require_relative '../lib/discourse_event'
require_relative '../lib/discourse_plugin' require_relative '../lib/discourse_plugin'
require_relative '../lib/discourse_plugin_registry' require_relative '../lib/discourse_plugin_registry'
require_relative '../lib/plugin_gem'
# Global config # Global config
require_relative '../app/models/global_setting' require_relative '../app/models/global_setting'
GlobalSetting.configure!
unless Rails.env.test? && ENV['LOAD_PLUGINS'] != "1"
require_relative '../lib/custom_setting_providers'
end
GlobalSetting.load_defaults
require 'pry-rails' if Rails.env.development? require 'pry-rails' if Rails.env.development?
@ -15,8 +22,10 @@ if defined?(Bundler)
Bundler.require(*Rails.groups(assets: %w(development test profile))) Bundler.require(*Rails.groups(assets: %w(development test profile)))
end end
module Discourse module Discourse
class Application < Rails::Application class Application < Rails::Application
def config.database_configuration def config.database_configuration
if Rails.env.production? if Rails.env.production?
GlobalSetting.database_config GlobalSetting.database_config

View File

@ -833,6 +833,7 @@ en:
none: "You haven't invited anyone here yet. You can send individual invites, or invite a bunch of people at once by <a href='https://meta.discourse.org/t/send-bulk-invites/16468'>uploading a CSV file</a>." none: "You haven't invited anyone here yet. You can send individual invites, or invite a bunch of people at once by <a href='https://meta.discourse.org/t/send-bulk-invites/16468'>uploading a CSV file</a>."
text: "Bulk Invite from File" text: "Bulk Invite from File"
success: "File uploaded successfully, you will be notified via message when the process is complete." success: "File uploaded successfully, you will be notified via message when the process is complete."
error: "Sorry, file should be of csv format."
password: password:
title: "Password" title: "Password"
@ -2409,8 +2410,8 @@ en:
backups: "backups" backups: "backups"
traffic_short: "Traffic" traffic_short: "Traffic"
traffic: "Application web requests" traffic: "Application web requests"
page_views: "API Requests" page_views: "Pageviews"
page_views_short: "API Requests" page_views_short: "Pageviews"
show_traffic_report: "Show Detailed Traffic Report" show_traffic_report: "Show Detailed Traffic Report"
reports: reports:
@ -2914,6 +2915,10 @@ en:
deleted_tag: "deleted tag" deleted_tag: "deleted tag"
renamed_tag: "renamed tag" renamed_tag: "renamed tag"
revoke_email: "revoke email" revoke_email: "revoke email"
lock_trust_level: "lock trust level"
unlock_trust_level: "unlock trust level"
activate_user: "activate user"
deactivate_user: "deactivate user"
screened_emails: screened_emails:
title: "Screened Emails" title: "Screened Emails"
description: "When someone tries to create a new account, the following email addresses will be checked and the registration will be blocked, or some other action performed." description: "When someone tries to create a new account, the following email addresses will be checked and the registration will be blocked, or some other action performed."

View File

@ -53,6 +53,7 @@ en:
purge_reason: "Automatically deleted as abandoned, deactivated account" purge_reason: "Automatically deleted as abandoned, deactivated account"
disable_remote_images_download_reason: "Remote images download was disabled because there wasn't enough disk space available." disable_remote_images_download_reason: "Remote images download was disabled because there wasn't enough disk space available."
anonymous: "Anonymous" anonymous: "Anonymous"
remove_posts_deleted_by_author: "Deleted by author"
emails: emails:
incoming: incoming:
@ -1615,6 +1616,8 @@ en:
user: user:
no_accounts_associated: "No accounts associated" no_accounts_associated: "No accounts associated"
deactivated: "Was deactivated due to too many bounced emails to '%{email}'." deactivated: "Was deactivated due to too many bounced emails to '%{email}'."
deactivated_by_staff: "Deactivated by staff"
activated_by_staff: "Activated by staff"
username: username:
short: "must be at least %{min} characters" short: "must be at least %{min} characters"
long: "must be no more than %{max} characters" long: "must be no more than %{max} characters"

View File

@ -701,7 +701,7 @@ files:
default: 3072 default: 3072
authorized_extensions: authorized_extensions:
client: true client: true
default: 'jpg|jpeg|png|gif|csv' default: 'jpg|jpeg|png|gif'
refresh: true refresh: true
type: list type: list
crawl_images: crawl_images:

View File

@ -52,7 +52,7 @@ class PostgreSQLFallbackHandler
logger.warn "#{log_prefix}: Master server is active. Reconnecting..." logger.warn "#{log_prefix}: Master server is active. Reconnecting..."
self.master_up(key) self.master_up(key)
Discourse.disable_readonly_mode Discourse.disable_readonly_mode(Discourse::PG_READONLY_MODE_KEY)
end end
rescue => e rescue => e
logger.warn "#{log_prefix}: Connection to master PostgreSQL server failed with '#{e.message}'" logger.warn "#{log_prefix}: Connection to master PostgreSQL server failed with '#{e.message}'"
@ -103,7 +103,7 @@ module ActiveRecord
})) }))
verify_replica(connection) verify_replica(connection)
Discourse.enable_readonly_mode Discourse.enable_readonly_mode(Discourse::PG_READONLY_MODE_KEY)
else else
begin begin
connection = postgresql_connection(config) connection = postgresql_connection(config)

View File

@ -0,0 +1,7 @@
# Support for plugins to register custom setting providers. They can do this
# by having a file, `register_provider.rb` in their root that will be run
# at this point.
Dir.glob(File.join(File.dirname(__FILE__), '../plugins', '*', "register_provider.rb")) do |p|
require p
end

View File

@ -113,24 +113,6 @@ module Discourse
end end
end end
def self.last_read_only
@last_read_only ||= {}
end
def self.recently_readonly?
read_only = last_read_only[$redis.namespace]
return false unless read_only
read_only > 15.seconds.ago
end
def self.received_readonly!
last_read_only[$redis.namespace] = Time.zone.now
end
def self.clear_readonly!
last_read_only[$redis.namespace] = nil
end
def self.disabled_plugin_names def self.disabled_plugin_names
plugins.select { |p| !p.enabled? }.map(&:name) plugins.select { |p| !p.enabled? }.map(&:name)
end end
@ -210,43 +192,66 @@ module Discourse
base_url_no_prefix + base_uri base_url_no_prefix + base_uri
end end
READONLY_MODE_KEY_TTL ||= 60 READONLY_MODE_KEY_TTL ||= 60
READONLY_MODE_KEY ||= 'readonly_mode'.freeze READONLY_MODE_KEY ||= 'readonly_mode'.freeze
PG_READONLY_MODE_KEY ||= 'readonly_mode:postgres'.freeze
USER_READONLY_MODE_KEY ||= 'readonly_mode:user'.freeze USER_READONLY_MODE_KEY ||= 'readonly_mode:user'.freeze
def self.enable_readonly_mode(user_enabled: false) READONLY_KEYS ||= [
if user_enabled READONLY_MODE_KEY,
$redis.set(USER_READONLY_MODE_KEY, 1) PG_READONLY_MODE_KEY,
USER_READONLY_MODE_KEY
]
def self.enable_readonly_mode(key = READONLY_MODE_KEY)
if key == USER_READONLY_MODE_KEY
$redis.set(key, 1)
else else
$redis.setex(READONLY_MODE_KEY, READONLY_MODE_KEY_TTL, 1) $redis.setex(key, READONLY_MODE_KEY_TTL, 1)
keep_readonly_mode keep_readonly_mode(key)
end end
MessageBus.publish(readonly_channel, true) MessageBus.publish(readonly_channel, true)
true true
end end
def self.keep_readonly_mode def self.keep_readonly_mode(key)
# extend the expiry by 1 minute every 30 seconds # extend the expiry by 1 minute every 30 seconds
unless Rails.env.test? unless Rails.env.test?
Thread.new do Thread.new do
while readonly_mode? while readonly_mode?
$redis.expire(READONLY_MODE_KEY, READONLY_MODE_KEY_TTL) $redis.expire(key, READONLY_MODE_KEY_TTL)
sleep 30.seconds sleep 30.seconds
end end
end end
end end
end end
def self.disable_readonly_mode(user_enabled: false) def self.disable_readonly_mode(key = READONLY_MODE_KEY)
key = user_enabled ? USER_READONLY_MODE_KEY : READONLY_MODE_KEY
$redis.del(key) $redis.del(key)
MessageBus.publish(readonly_channel, false) MessageBus.publish(readonly_channel, false)
true true
end end
def self.readonly_mode? def self.readonly_mode?
recently_readonly? || !!$redis.get(READONLY_MODE_KEY) || !!$redis.get(USER_READONLY_MODE_KEY) recently_readonly? || READONLY_KEYS.any? { |key| !!$redis.get(key) }
end
def self.last_read_only
@last_read_only ||= {}
end
def self.recently_readonly?
return false unless read_only = last_read_only[$redis.namespace]
read_only > 15.seconds.ago
end
def self.received_readonly!
last_read_only[$redis.namespace] = Time.zone.now
end
def self.clear_readonly!
last_read_only[$redis.namespace] = nil
end end
def self.request_refresh! def self.request_refresh!

View File

@ -156,7 +156,7 @@ module Email
elsif bounce_score >= SiteSetting.bounce_score_threshold elsif bounce_score >= SiteSetting.bounce_score_threshold
# NOTE: we check bounce_score before sending emails, nothing to do # NOTE: we check bounce_score before sending emails, nothing to do
# here other than log it happened. # here other than log it happened.
reason = I18n.t("user.email.revoked", email: user.email, date: user.user_stat.reset_bounce_score_after) reason = I18n.t("user.email.revoked", date: user.user_stat.reset_bounce_score_after)
StaffActionLogger.new(Discourse.system_user).log_revoke_email(user, reason) StaffActionLogger.new(Discourse.system_user).log_revoke_email(user, reason)
end end
end end
@ -239,6 +239,8 @@ module Email
end end
def parse_from_field(mail) def parse_from_field(mail)
return unless mail[:from]
if mail[:from].errors.blank? if mail[:from].errors.blank?
mail[:from].address_list.addresses.each do |address_field| mail[:from].address_list.addresses.each do |address_field|
address_field.decoded address_field.decoded

View File

@ -363,27 +363,7 @@ JS
# #
# This is a very rough initial implementation # This is a very rough initial implementation
def gem(name, version, opts = {}) def gem(name, version, opts = {})
gems_path = File.dirname(path) + "/gems/#{RUBY_VERSION}" PluginGem.load(path, name, version, opts)
spec_path = gems_path + "/specifications"
spec_file = spec_path + "/#{name}-#{version}.gemspec"
unless File.exists? spec_file
command = "gem install #{name} -v #{version} -i #{gems_path} --no-document --ignore-dependencies"
if opts[:source]
command << " --source #{opts[:source]}"
end
puts command
puts `#{command}`
end
if File.exists? spec_file
spec = Gem::Specification.load spec_file
spec.activate
unless opts[:require] == false
require opts[:require_name] ? opts[:require_name] : name
end
else
puts "You are specifying the gem #{name} in #{path}, however it does not exist!"
exit(-1)
end
end end
def enabled_site_setting(setting=nil) def enabled_site_setting(setting=nil)

27
lib/plugin_gem.rb Normal file
View File

@ -0,0 +1,27 @@
module PluginGem
def self.load(path, name, version, opts=nil)
opts ||= {}
gems_path = File.dirname(path) + "/gems/#{RUBY_VERSION}"
spec_path = gems_path + "/specifications"
spec_file = spec_path + "/#{name}-#{version}.gemspec"
unless File.exists? spec_file
command = "gem install #{name} -v #{version} -i #{gems_path} --no-document --ignore-dependencies"
if opts[:source]
command << " --source #{opts[:source]}"
end
puts command
puts `#{command}`
end
if File.exists? spec_file
spec = Gem::Specification.load spec_file
spec.activate
unless opts[:require] == false
require opts[:require_name] ? opts[:require_name] : name
end
else
puts "You are specifying the gem #{name} in #{path}, however it does not exist!"
exit(-1)
end
end
end

View File

@ -29,7 +29,7 @@ class PostDestroyer
pa.post_action_type_id IN (?) pa.post_action_type_id IN (?)
)", PostActionType.notify_flag_type_ids) )", PostActionType.notify_flag_type_ids)
.each do |post| .each do |post|
PostDestroyer.new(Discourse.system_user, post).destroy PostDestroyer.new(Discourse.system_user, post, {context: I18n.t('remove_posts_deleted_by_author')}).destroy
end end
end end

View File

@ -50,7 +50,7 @@ module PrettyText
topic = Topic.find_by(id: topic_id) topic = Topic.find_by(id: topic_id)
if topic && Guardian.new.can_see?(topic) if topic && Guardian.new.can_see?(topic)
{ {
title: topic.title, title: Rack::Utils.escape_html(topic.title),
href: topic.url href: topic.url
} }
end end

View File

@ -16,6 +16,8 @@ class ImportScripts::Bbpress < ImportScripts::Base
BATCH_SIZE ||= 1000 BATCH_SIZE ||= 1000
BB_PRESS_PW ||= ENV['BBPRESS_PW'] || "" BB_PRESS_PW ||= ENV['BBPRESS_PW'] || ""
BB_PRESS_USER ||= ENV['BBPRESS_USER'] || "root" BB_PRESS_USER ||= ENV['BBPRESS_USER'] || "root"
BB_PRESS_PREFIX ||= ENV['BBPRESS_PREFIX'] || "wp_"
def initialize def initialize
super super
@ -37,12 +39,12 @@ class ImportScripts::Bbpress < ImportScripts::Base
puts "", "importing users..." puts "", "importing users..."
last_user_id = -1 last_user_id = -1
total_users = bbpress_query("SELECT COUNT(*) count FROM wp_users WHERE user_email LIKE '%@%'").first["count"] total_users = bbpress_query("SELECT COUNT(*) count FROM #{BB_PRESS_PREFIX}users WHERE user_email LIKE '%@%'").first["count"]
batches(BATCH_SIZE) do |offset| batches(BATCH_SIZE) do |offset|
users = bbpress_query(<<-SQL users = bbpress_query(<<-SQL
SELECT id, user_nicename, display_name, user_email, user_registered, user_url SELECT id, user_nicename, display_name, user_email, user_registered, user_url
FROM wp_users FROM #{BB_PRESS_PREFIX}users
WHERE user_email LIKE '%@%' WHERE user_email LIKE '%@%'
AND id > #{last_user_id} AND id > #{last_user_id}
ORDER BY id ORDER BY id
@ -62,7 +64,7 @@ class ImportScripts::Bbpress < ImportScripts::Base
users_description = {} users_description = {}
bbpress_query(<<-SQL bbpress_query(<<-SQL
SELECT user_id, meta_value description SELECT user_id, meta_value description
FROM wp_usermeta FROM #{BB_PRESS_PREFIX}usermeta
WHERE user_id IN (#{user_ids_sql}) WHERE user_id IN (#{user_ids_sql})
AND meta_key = 'description' AND meta_key = 'description'
SQL SQL
@ -71,7 +73,7 @@ class ImportScripts::Bbpress < ImportScripts::Base
users_last_activity = {} users_last_activity = {}
bbpress_query(<<-SQL bbpress_query(<<-SQL
SELECT user_id, meta_value last_activity SELECT user_id, meta_value last_activity
FROM wp_usermeta FROM #{BB_PRESS_PREFIX}usermeta
WHERE user_id IN (#{user_ids_sql}) WHERE user_id IN (#{user_ids_sql})
AND meta_key = 'last_activity' AND meta_key = 'last_activity'
SQL SQL
@ -97,7 +99,7 @@ class ImportScripts::Bbpress < ImportScripts::Base
categories = bbpress_query(<<-SQL categories = bbpress_query(<<-SQL
SELECT id, post_name, post_parent SELECT id, post_name, post_parent
FROM wp_posts FROM #{BB_PRESS_PREFIX}posts
WHERE post_type = 'forum' WHERE post_type = 'forum'
AND LENGTH(COALESCE(post_name, '')) > 0 AND LENGTH(COALESCE(post_name, '')) > 0
ORDER BY post_parent, id ORDER BY post_parent, id
@ -119,7 +121,7 @@ class ImportScripts::Bbpress < ImportScripts::Base
last_post_id = -1 last_post_id = -1
total_posts = bbpress_query(<<-SQL total_posts = bbpress_query(<<-SQL
SELECT COUNT(*) count SELECT COUNT(*) count
FROM wp_posts FROM #{BB_PRESS_PREFIX}posts
WHERE post_status <> 'spam' WHERE post_status <> 'spam'
AND post_type IN ('topic', 'reply') AND post_type IN ('topic', 'reply')
SQL SQL
@ -134,7 +136,7 @@ class ImportScripts::Bbpress < ImportScripts::Base
post_title, post_title,
post_type, post_type,
post_parent post_parent
FROM wp_posts FROM #{BB_PRESS_PREFIX}posts
WHERE post_status <> 'spam' WHERE post_status <> 'spam'
AND post_type IN ('topic', 'reply') AND post_type IN ('topic', 'reply')
AND id > #{last_post_id} AND id > #{last_post_id}
@ -155,7 +157,7 @@ class ImportScripts::Bbpress < ImportScripts::Base
posts_likes = {} posts_likes = {}
bbpress_query(<<-SQL bbpress_query(<<-SQL
SELECT post_id, meta_value likes SELECT post_id, meta_value likes
FROM wp_postmeta FROM #{BB_PRESS_PREFIX}postmeta
WHERE post_id IN (#{post_ids_sql}) WHERE post_id IN (#{post_ids_sql})
AND meta_key = 'Likes' AND meta_key = 'Likes'
SQL SQL

View File

@ -0,0 +1,210 @@
require "mysql2"
require File.expand_path(File.dirname(__FILE__) + "/base.rb")
class ImportScripts::Drupal < ImportScripts::Base
DRUPAL_DB = ENV['DRUPAL_DB'] || "newsite3"
VID = ENV['DRUPAL_VID'] || 1
def initialize
super
@client = Mysql2::Client.new(
host: "localhost",
username: "root",
#password: "password",
database: DRUPAL_DB
)
end
def categories_query
@client.query("SELECT tid, name, description FROM term_data WHERE vid = #{VID}")
end
def execute
create_users(@client.query("SELECT uid id, name, mail email, created FROM users;")) do |row|
{id: row['id'], username: row['name'], email: row['email'], created_at: Time.zone.at(row['created'])}
end
# You'll need to edit the following query for your Drupal install:
#
# * Drupal allows duplicate category names, so you may need to exclude some categories or rename them here.
# * Table name may be term_data.
# * May need to select a vid other than 1.
create_categories(categories_query) do |c|
{id: c['tid'], name: c['name'], description: c['description']}
end
# "Nodes" in Drupal are divided into types. Here we import two types,
# and will later import all the comments/replies for each node.
# You will need to figure out what the type names are on your install and edit the queries to match.
if ENV['DRUPAL_IMPORT_BLOG']
create_blog_topics
end
create_forum_topics
create_replies
begin
create_admin(email: 'neil.lalonde@discourse.org', username: UserNameSuggester.suggest('neil'))
rescue => e
puts '', "Failed to create admin user"
puts e.message
end
end
def create_blog_topics
puts '', "creating blog topics"
create_category({
name: 'Blog',
user_id: -1,
description: "Articles from the blog"
}, nil) unless Category.find_by_name('Blog')
results = @client.query("
SELECT n.nid nid,
n.title title,
n.uid uid,
n.created created,
n.sticky sticky,
nr.body body
FROM node n
LEFT JOIN node_revisions nr ON nr.vid=n.vid
WHERE n.type = 'blog'
AND n.status = 1
", cache_rows: false)
create_posts(results) do |row|
{
id: "nid:#{row['nid']}",
user_id: user_id_from_imported_user_id(row['uid']) || -1,
category: 'Blog',
raw: row['body'],
created_at: Time.zone.at(row['created']),
pinned_at: row['sticky'].to_i == 1 ? Time.zone.at(row['created']) : nil,
title: row['title'].try(:strip),
custom_fields: {import_id: "nid:#{row['nid']}"}
}
end
end
def create_forum_topics
puts '', "creating forum topics"
total_count = @client.query("
SELECT COUNT(*) count
FROM node n
LEFT JOIN forum f ON f.vid=n.vid
WHERE n.type = 'forum'
AND n.status = 1
").first['count']
batch_size = 1000
batches(batch_size) do |offset|
results = @client.query("
SELECT n.nid nid,
n.title title,
f.tid tid,
n.uid uid,
n.created created,
n.sticky sticky,
nr.body body
FROM node n
LEFT JOIN forum f ON f.vid=n.vid
LEFT JOIN node_revisions nr ON nr.vid=n.vid
WHERE node.type = 'forum'
AND node.status = 1
LIMIT #{batch_size}
OFFSET #{offset};
", cache_rows: false)
break if results.size < 1
next if all_records_exist? :posts, results.map {|p| "nid:#{p['nid']}"}
create_posts(results, total: total_count, offset: offset) do |row|
{
id: "nid:#{row['nid']}",
user_id: user_id_from_imported_user_id(row['uid']) || -1,
category: category_id_from_imported_category_id(row['tid']),
raw: row['body'],
created_at: Time.zone.at(row['created']),
pinned_at: row['sticky'].to_i == 1 ? Time.zone.at(row['created']) : nil,
title: row['title'].try(:strip)
}
end
end
end
def create_replies
puts '', "creating replies in topics"
if ENV['DRUPAL_IMPORT_BLOG']
node_types = "('forum','blog')"
else
node_types = "('forum')"
end
total_count = @client.query("
SELECT COUNT(*) count
FROM comments c
LEFT JOIN node n ON n.nid=c.nid
WHERE node.type IN #{node_types}
AND node.status = 1
AND comments.status=0;
").first['count']
batch_size = 1000
batches(batch_size) do |offset|
results = @client.query("
SELECT c.cid,
c.pid,
c.nid,
c.uid,
c.timestamp,
c.comment body
FROM comments c
LEFT JOIN node n ON n.nid=c.nid
WHERE n.type IN #{node_types}
AND n.status = 1
AND c.status=0
LIMIT #{batch_size}
OFFSET #{offset};
", cache_rows: false)
break if results.size < 1
next if all_records_exist? :posts, results.map {|p| "cid:#{p['cid']}"}
create_posts(results, total: total_count, offset: offset) do |row|
topic_mapping = topic_lookup_from_imported_post_id("nid:#{row['nid']}")
if topic_mapping && topic_id = topic_mapping[:topic_id]
h = {
id: "cid:#{row['cid']}",
topic_id: topic_id,
user_id: user_id_from_imported_user_id(row['uid']) || -1,
raw: row['body'],
created_at: Time.zone.at(row['timestamp']),
}
if row['pid']
parent = topic_lookup_from_imported_post_id("cid:#{row['pid']}")
h[:reply_to_post_number] = parent[:post_number] if parent and parent[:post_number] > 1
end
h
else
puts "No topic found for comment #{row['cid']}"
nil
end
end
end
end
end
if __FILE__==$0
ImportScripts::Drupal.new.perform
end

View File

@ -42,8 +42,13 @@ describe ActiveRecord::ConnectionHandling do
end end
after do after do
with_multisite_db(multisite_db) { Discourse.disable_readonly_mode } pg_readonly_mode_key = Discourse::PG_READONLY_MODE_KEY
Discourse.disable_readonly_mode
with_multisite_db(multisite_db) do
Discourse.disable_readonly_mode(pg_readonly_mode_key)
end
Discourse.disable_readonly_mode(pg_readonly_mode_key)
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env]) ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env])
end end

View File

@ -118,7 +118,7 @@ describe Discourse do
context 'user enabled readonly mode' do context 'user enabled readonly mode' do
it "adds a key in redis and publish a message through the message bus" do it "adds a key in redis and publish a message through the message bus" do
expect($redis.get(user_readonly_mode_key)).to eq(nil) expect($redis.get(user_readonly_mode_key)).to eq(nil)
message = MessageBus.track_publish { Discourse.enable_readonly_mode(user_enabled: true) }.first message = MessageBus.track_publish { Discourse.enable_readonly_mode(user_readonly_mode_key) }.first
assert_readonly_mode(message, user_readonly_mode_key) assert_readonly_mode(message, user_readonly_mode_key)
end end
end end
@ -160,10 +160,10 @@ describe Discourse do
end end
it "returns true when user enabled readonly mode key is present in redis" do it "returns true when user enabled readonly mode key is present in redis" do
Discourse.enable_readonly_mode(user_enabled: true) Discourse.enable_readonly_mode(user_readonly_mode_key)
expect(Discourse.readonly_mode?).to eq(true) expect(Discourse.readonly_mode?).to eq(true)
Discourse.disable_readonly_mode(user_enabled: true) Discourse.disable_readonly_mode(user_readonly_mode_key)
expect(Discourse.readonly_mode?).to eq(false) expect(Discourse.readonly_mode?).to eq(false)
end end
end end

View File

@ -383,37 +383,46 @@ describe Email::Receiver do
expect(Post.last.raw).to match(/discourse\.rb/) expect(Post.last.raw).to match(/discourse\.rb/)
end end
it "handles forwarded emails" do context "with forwarded emails enabled" do
SiteSetting.enable_forwarded_emails = true before { SiteSetting.enable_forwarded_emails = true }
expect { process(:forwarded_email_1) }.to change(Topic, :count)
forwarded_post, last_post = *Post.last(2) it "handles forwarded emails" do
expect { process(:forwarded_email_1) }.to change(Topic, :count)
expect(forwarded_post.user.email).to eq("some@one.com") forwarded_post, last_post = *Post.last(2)
expect(last_post.user.email).to eq("ba@bar.com")
expect(forwarded_post.raw).to match(/XoXo/) expect(forwarded_post.user.email).to eq("some@one.com")
expect(last_post.raw).to match(/can you have a look at this email below/) expect(last_post.user.email).to eq("ba@bar.com")
expect(last_post.post_type).to eq(Post.types[:regular]) expect(forwarded_post.raw).to match(/XoXo/)
end expect(last_post.raw).to match(/can you have a look at this email below/)
it "handles weirdly forwarded emails" do expect(last_post.post_type).to eq(Post.types[:regular])
group.add(Fabricate(:user, email: "ba@bar.com")) end
group.save
SiteSetting.enable_forwarded_emails = true it "handles weirdly forwarded emails" do
expect { process(:forwarded_email_2) }.to change(Topic, :count) group.add(Fabricate(:user, email: "ba@bar.com"))
group.save
forwarded_post, last_post = *Post.last(2) SiteSetting.enable_forwarded_emails = true
expect { process(:forwarded_email_2) }.to change(Topic, :count)
expect(forwarded_post.user.email).to eq("some@one.com") forwarded_post, last_post = *Post.last(2)
expect(last_post.user.email).to eq("ba@bar.com")
expect(forwarded_post.raw).to match(/XoXo/) expect(forwarded_post.user.email).to eq("some@one.com")
expect(last_post.raw).to match(/can you have a look at this email below/) expect(last_post.user.email).to eq("ba@bar.com")
expect(forwarded_post.raw).to match(/XoXo/)
expect(last_post.raw).to match(/can you have a look at this email below/)
expect(last_post.post_type).to eq(Post.types[:whisper])
end
# Who thought this was a good idea?!
it "doesn't blow up with localized email headers" do
expect { process(:forwarded_email_3) }.to change(Topic, :count)
end
expect(last_post.post_type).to eq(Post.types[:whisper])
end end
end end

View File

@ -10,10 +10,10 @@ describe PrettyText do
describe "off topic quoting" do describe "off topic quoting" do
it "can correctly populate topic title" do it "can correctly populate topic title" do
topic = Fabricate(:topic, title: "this is a test topic") topic = Fabricate(:topic, title: "this is a test topic :slight_smile:")
expected = <<HTML expected = <<HTML
<aside class="quote" data-post="2" data-topic="#{topic.id}"><div class="title"> <aside class="quote" data-post="2" data-topic="#{topic.id}"><div class="title">
<div class="quote-controls"></div><a href="http://test.localhost/t/this-is-a-test-topic/#{topic.id}/2">This is a test topic</a> <div class="quote-controls"></div><a href="http://test.localhost/t/this-is-a-test-topic-slight-smile/#{topic.id}/2">This is a test topic <img src="/images/emoji/emoji_one/slight_smile.png?v=3" title="slight_smile" alt="slight_smile" class="emoji"></a>
</div> </div>
<blockquote><p>ddd</p></blockquote></aside> <blockquote><p>ddd</p></blockquote></aside>
HTML HTML

View File

@ -0,0 +1,18 @@
Message-ID: <60@foo.bar.mail>
From: Ba Bar <ba@bar.com>
To: Team <team@bar.com>
Date: Mon, 9 Dec 2016 13:37:42 +0100
Subject: Fwd: Ça Discourse ?
@team, can you have a look at this email below?
Objet: Ça Discourse ?
Date: 2017-01-04 11:27
De: Un Français <un@francais.fr>
À: ba@bar.com
Bonjour,
Ça Discourse bien aujourd'hui ?
Bises

View File

@ -369,4 +369,42 @@ describe StaffActionLogger do
expect(user_history.action).to eq(UserHistory.actions[:create_category]) expect(user_history.action).to eq(UserHistory.actions[:create_category])
end end
end end
describe 'log_lock_trust_level' do
let(:user) { Fabricate(:user) }
it "raises an error when argument is missing" do
expect { logger.log_lock_trust_level(nil) }.to raise_error(Discourse::InvalidParameters)
end
it "creates a new UserHistory record" do
user.trust_level_locked = true
expect { logger.log_lock_trust_level(user) }.to change { UserHistory.count }.by(1)
user_history = UserHistory.last
expect(user_history.action).to eq(UserHistory.actions[:lock_trust_level])
user.trust_level_locked = false
expect { logger.log_lock_trust_level(user) }.to change { UserHistory.count }.by(1)
user_history = UserHistory.last
expect(user_history.action).to eq(UserHistory.actions[:unlock_trust_level])
end
end
describe 'log_user_activate' do
let(:user) { Fabricate(:user) }
it "raises an error when argument is missing" do
expect { logger.log_user_activate(nil, nil) }.to raise_error(Discourse::InvalidParameters)
end
it "creates a new UserHistory record" do
reason = "Staff activated from admin"
expect {
logger.log_user_activate(user, reason)
}.to change { UserHistory.count }.by(1)
user_history = UserHistory.last
expect(user_history.action).to eq(UserHistory.actions[:activate_user])
expect(user_history.details).to eq(reason)
end
end
end end

View File

@ -58,6 +58,14 @@ describe UserBlocker do
SystemMessage.expects(:create).never SystemMessage.expects(:create).never
expect(block_user).to eq(false) expect(block_user).to eq(false)
end end
it "logs it with context" do
SystemMessage.stubs(:create).returns(Fabricate.build(:post))
expect {
UserBlocker.block(user, Fabricate(:admin))
}.to change { UserHistory.count }.by(1)
expect(UserHistory.last.context).to be_present
end
end end
describe 'unblock' do describe 'unblock' do
@ -81,6 +89,12 @@ describe UserBlocker do
SystemMessage.expects(:create).never SystemMessage.expects(:create).never
unblock_user unblock_user
end end
it "logs it" do
expect {
unblock_user
}.to change { UserHistory.count }.by(1)
end
end end
describe 'hide_posts' do describe 'hide_posts' do