Merge branch 'master' into fix_whisper

This commit is contained in:
Sam 2016-12-02 17:44:05 +11:00
commit 9b885c039a
40 changed files with 286 additions and 210 deletions

View File

@ -55,4 +55,4 @@ install:
- bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails rails-observers seed-fu; fi"
- bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3; fi"
script: 'bundle exec rspec && bundle exec rake plugin:spec && bundle exec rake qunit:test'
script: "bundle exec rspec && bundle exec rake plugin:spec && bundle exec rake qunit:test['200000']"

View File

@ -212,7 +212,7 @@ GEM
omniauth-twitter (1.2.1)
json (~> 1.3)
omniauth-oauth (~> 1.1)
onebox (1.6.1)
onebox (1.6.2)
htmlentities (~> 4.3.4)
moneta (~> 0.8)
multi_json (~> 1.11)

View File

@ -1,6 +1,7 @@
import debounce from 'discourse/lib/debounce';
export default Ember.Controller.extend({
queryParams: ["filter"],
filter: null,
onlyOverridden: false,
filtered: Ember.computed.notEmpty('filter'),

View File

@ -6,12 +6,8 @@ export default Ember.Controller.extend({
fieldTypes: null,
createDisabled: Em.computed.gte('model.length', MAX_FIELDS),
arrangedContent: function() {
return Ember.ArrayProxy.extend(Ember.SortableMixin).create({
sortProperties: ['position'],
content: this.get('model')
});
}.property('model'),
fieldSortOrder: ['position'],
sortedFields: Ember.computed.sort('model', 'fieldSortOrder'),
actions: {
createField() {
@ -20,9 +16,9 @@ export default Ember.Controller.extend({
},
moveUp(f) {
const idx = this.get('arrangedContent').indexOf(f);
const idx = this.get('sortedFields').indexOf(f);
if (idx) {
const prev = this.get('arrangedContent').objectAt(idx-1);
const prev = this.get('sortedFields').objectAt(idx-1);
const prevPos = prev.get('position');
prev.update({ position: f.get('position') });
@ -31,9 +27,9 @@ export default Ember.Controller.extend({
},
moveDown(f) {
const idx = this.get('arrangedContent').indexOf(f);
const idx = this.get('sortedFields').indexOf(f);
if (idx > -1) {
const next = this.get('arrangedContent').objectAt(idx+1);
const next = this.get('sortedFields').objectAt(idx+1);
const nextPos = next.get('position');
next.update({ position: f.get('position') });

View File

@ -83,6 +83,8 @@ export default function() {
this.route('show', { path: '/:badge_id' });
});
this.route('adminPlugins', { path: '/plugins', resetNamespace: true });
this.route('adminPlugins', { path: '/plugins', resetNamespace: true }, function() {
this.route('index', { path: '/' });
});
});
};

View File

@ -4,11 +4,11 @@
<p class="desc">{{i18n 'admin.user_fields.help'}}</p>
{{#if model}}
{{#each arrangedContent as |uf|}}
{{#each sortedFields as |uf|}}
{{admin-user-field-item userField=uf
fieldTypes=fieldTypes
firstField=arrangedContent.firstObject
lastField=arrangedContent.lastObject
firstField=sortedFields.firstObject
lastField=sortedFields.lastObject
destroyAction="destroy"
moveUpAction="moveUp"
moveDownAction="moveDown"}}

View File

@ -135,7 +135,7 @@ export function buildResolver(baseName) {
},
findPluginTemplate(parsedName) {
var pluginParsedName = this.parseName(parsedName.fullName.replace("template:", "template:javascripts/"));
const pluginParsedName = this.parseName(parsedName.fullName.replace("template:", "template:javascripts/"));
return this.findTemplate(pluginParsedName);
},

View File

@ -28,7 +28,7 @@ export default Ember.Component.extend({
}
}
this.appEvents.trigger('modal:body-shown', this.getProperties('title'));
this.appEvents.trigger('modal:body-shown', this.getProperties('title', 'rawTitle'));
},
_flash(msg) {

View File

@ -19,6 +19,8 @@ export default Ember.Component.extend({
this.appEvents.on('modal:body-shown', data => {
if (data.title) {
this.set('title', I18n.t(data.title));
} else if (data.rawTitle) {
this.set('title', data.rawTitle);
}
});
},

View File

@ -65,7 +65,7 @@ export default Ember.Component.extend({
const prevEvent = this.get('prevEvent');
if (prevEvent) {
this._topicScrolled(prevEvent);
Ember.run.scheduleOnce('afterRender', this, this._topicScrolled, prevEvent);
} else {
Ember.run.scheduleOnce('afterRender', this, this._updateProgressBar);
}

View File

@ -27,8 +27,6 @@ export default Ember.Component.extend(bufferedRender({
}.property('disableActions'),
buildBuffer(buffer) {
const self = this;
const renderIcon = function(name, key, actionable) {
const title = escapeExpression(I18n.t(`topic_statuses.${key}.help`)),
startTag = actionable ? "a href" : "span",
@ -39,8 +37,8 @@ export default Ember.Component.extend(bufferedRender({
buffer.push(`<${startTag} title='${title}' class='topic-status'>${icon}</${endTag}>`);
};
const renderIconIf = function(conditionProp, name, key, actionable) {
if (!self.get(conditionProp)) { return; }
const renderIconIf = (conditionProp, name, key, actionable) => {
if (!this.get(conditionProp)) { return; }
renderIcon(name, key, actionable);
};

View File

@ -12,7 +12,7 @@ export default DiscoveryController.extend({
return Discourse.User.currentProp('staff');
},
@computed("model.categories.@each.featuredTopics.length")
@computed("model.categories.[].featuredTopics.length")
latestTopicOnly() {
return this.get("model.categories").find(c => c.get("featuredTopics.length") > 1) === undefined;
},

View File

@ -157,9 +157,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
// Cook the bio for preview
model.set('name', this.get('newNameInput'));
var options = {};
return model.save(options).then(() => {
return model.save().then(() => {
if (Discourse.User.currentProp('id') === model.get('id')) {
Discourse.User.currentProp('name', model.get('name'));
}

View File

@ -5,8 +5,6 @@ import { popupAjaxError } from 'discourse/lib/ajax-error';
import { on, default as computed } from "ember-addons/ember-computed-decorators";
import Ember from 'ember';
const SortableArrayProxy = Ember.ArrayProxy.extend(Ember.SortableMixin);
export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, {
@on('init')
@ -20,12 +18,8 @@ export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, {
return categories.map(c => bufProxy.create({ content: c }));
},
categoriesOrdered: function() {
return SortableArrayProxy.create({
sortProperties: ['content.position'],
content: this.get('categoriesBuffered')
});
}.property('categoriesBuffered'),
categoriesSorting: ['position'],
categoriesOrdered: Ember.computed.sort('categoriesBuffered', 'categoriesSorting'),
showFixIndices: function() {
const cats = this.get('categoriesOrdered');

View File

@ -234,7 +234,7 @@ export function uploadLocation(url) {
} else {
var protocol = window.location.protocol + '//',
hostname = window.location.hostname,
port = ':' + window.location.port;
port = window.location.port ? ':' + window.location.port : '';
return protocol + hostname + port + url;
}
}

View File

@ -146,10 +146,9 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
},
changeBulkTemplate(w) {
const controllerName = w.replace('modal/', ''),
factory = getOwner(this).lookupFactory('controller:' + controllerName);
this.render(w, {into: 'modal/topic-bulk-actions', outlet: 'bulkOutlet', controller: factory ? controllerName : 'topic-bulk-actions'});
const controllerName = w.replace('modal/', '');
const controller = getOwner(this).lookup('controller:' + controllerName);
this.render(w, {into: 'modal/topic-bulk-actions', outlet: 'bulkOutlet', controller: controller ? controllerName : 'topic-bulk-actions'});
},
createNewTopicViaParams(title, body, category_id, category, tags) {

View File

@ -3,5 +3,5 @@ import UserAction from "discourse/models/user-action";
export default UserActivityStreamRoute.extend({
userActionType: UserAction.TYPES["likes_given"],
noContentHelpKey: 'no_likes_given'
noContentHelpKey: 'user_activity.no_likes_given'
});

View File

@ -136,7 +136,7 @@ $tag-color: scale-color($primary, $lightness: 40%);
top: -0.1em;
}
header .discourse-tag {color: $tag-color !important; }
header .discourse-tag {color: $tag-color }
.list-tags {
display: inline;

View File

@ -130,13 +130,15 @@ class UserNotifications < ActionMailer::Base
end
# Now fetch some topics and posts to show
topics_for_digest = Topic.for_digest(user, min_date, limit: SiteSetting.digest_topics + 3, top_order: true).to_a
topics_for_digest = Topic.for_digest(user, min_date, limit: SiteSetting.digest_topics + SiteSetting.digest_other_topics, top_order: true).to_a
@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.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, 5.0)
.order("posts.score DESC")
.limit(SiteSetting.digest_posts)

View File

@ -20,22 +20,22 @@ class QuotedPost < ActiveRecord::Base
next if uniq[[topic_id,post_number]]
uniq[[topic_id,post_number]] = true
begin
# It would be so much nicer if we used post_id in quotes
results = exec_sql "INSERT INTO quoted_posts(post_id, quoted_post_id, created_at, updated_at)
SELECT :post_id, p.id, current_timestamp, current_timestamp
FROM posts p
LEFT JOIN quoted_posts q on q.post_id = :post_id AND q.quoted_post_id = p.id
WHERE post_number = :post_number AND
topic_id = :topic_id AND
q.id IS NULL
RETURNING quoted_post_id
", post_id: post.id, post_number: post_number, topic_id: topic_id
# It would be so much nicer if we used post_id in quotes
results = exec_sql "INSERT INTO quoted_posts(post_id, quoted_post_id, created_at, updated_at)
SELECT :post_id, p.id, current_timestamp, current_timestamp
FROM posts p
LEFT JOIN quoted_posts q on q.post_id = :post_id AND q.quoted_post_id = p.id
WHERE post_number = :post_number AND
topic_id = :topic_id AND
q.id IS NULL
RETURNING quoted_post_id
", post_id: post.id, post_number: post_number, topic_id: topic_id
results = results.to_a
if results.length > 0
ids << results[0]["quoted_post_id"].to_i
results = results.to_a
ids << results[0]["quoted_post_id"].to_i if results.length > 0
rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation
# it's fine
end
end

View File

@ -37,7 +37,7 @@ class TopicLink < ActiveRecord::Base
def self.topic_map(guardian, topic_id)
# Sam: complicated reports are really hard in AR
builder = SqlBuilder.new <<SQL
builder = SqlBuilder.new <<-SQL
SELECT ftl.url,
COALESCE(ft.title, ftl.title) AS title,
ftl.link_topic_id,
@ -163,20 +163,20 @@ SQL
added_urls << url
topic_link = TopicLink.find_by(topic_id: post.topic_id,
post_id: post.id,
url: url)
unless topic_link
TopicLink.create!(post_id: post.id,
user_id: post.user_id,
topic_id: post.topic_id,
url: url,
domain: parsed.host || Discourse.current_hostname,
internal: internal,
link_topic_id: topic_id,
link_post_id: reflected_post.try(:id),
quote: link.is_quote)
unless TopicLink.exists?(topic_id: post.topic_id, post_id: post.id, url: url)
begin
TopicLink.create!(post_id: post.id,
user_id: post.user_id,
topic_id: post.topic_id,
url: url,
domain: parsed.host || Discourse.current_hostname,
internal: internal,
link_topic_id: topic_id,
link_post_id: reflected_post.try(:id),
quote: link.is_quote)
rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation
# it's fine
end
end
# Create the reflection if we can
@ -184,17 +184,14 @@ SQL
topic = Topic.find_by(id: topic_id)
if topic && post.topic && post.topic.archetype != 'private_message' && topic.archetype != 'private_message'
prefix = Discourse.base_url_no_prefix
reflected_url = "#{prefix}#{post.topic.relative_url(post.post_number)}"
tl = TopicLink.find_by(topic_id: topic_id,
post_id: reflected_post.try(:id),
url: reflected_url)
unless tl
tl = TopicLink.create!(user_id: post.user_id,
tl = TopicLink.create(user_id: post.user_id,
topic_id: topic_id,
post_id: reflected_post.try(:id),
url: reflected_url,
@ -206,7 +203,7 @@ SQL
end
reflected_ids << tl.try(:id)
reflected_ids << tl.id if tl.persisted?
end
end

View File

@ -129,13 +129,15 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
<img src="<%= t.user.small_avatar_url -%>" style="border-radius:50%;clear:both;display:block;float:none;height:50px;width:50px;margin:0;max-width:100%;outline:0;text-align:center;text-decoration:none;" align="center">
</td>
<td style="color:#0a0a0a;padding:0 16px 0 8px;text-align:left;">
<h6 style="color:inherit;font-size:18px;font-weight:400;line-height:1.3;margin:0;padding:0;word-wrap:normal;"><%= t.user.try(:username) -%></h6>
<% if t.user.try(:name).present? %>
<p style="color:#8f8f8f;line-height:1.3;margin:0 0 16px 0;padding:0;"><%= t.user.name -%></p>
<% if t.user %>
<h6 style="color:inherit;font-size:18px;font-weight:400;line-height:1.3;margin:0;padding:0;word-wrap:normal;"><%= t.user.username -%></h6>
<% if SiteSetting.enable_names? && t.user.name.present? && t.user.name.downcase != t.user.username.downcase %>
<p style="color:#8f8f8f;line-height:1.3;margin:0 0 16px 0;padding:0;"><%= t.user.name -%></p>
<% end %>
<% end %>
</td>
<%- if show_image_with_url(t.image_url) -%>
<td style="margin:0;padding:0 16px 8px 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;">
</td>
<%- end -%>
@ -147,7 +149,7 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
<table style="border-bottom:1px solid #f3f3f3;padding:0;text-align:left;vertical-align:top;width:100%">
<tbody>
<tr>
<td style="color:#0a0a0a;line-height:1.3;padding:0 16px 16px 16px;text-align:left;width:100%;font-weight:normal;">
<td style="color:#0a0a0a;line-height:1.3;padding:0 16px 0 16px;text-align:left;width:100%;font-weight:normal;">
<%= email_excerpt(t.first_post.cooked) %>
</td>
</tr>
@ -219,7 +221,7 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
<table style="background-color: #f3f3f3; width: 100%;">
<tr>
<td>
<h1 style="color:#0a0a0a;font-size:28px;line-height:1.3;text-align:center;">
<h1 style="color:#0a0a0a;font-size:28px;line-height:1.3;text-align:center;font-family:Helvetica,Arial,sans-serif;">
<%=t 'user_notifications.digest.popular_posts' %>
</h1>
</td>
@ -261,8 +263,12 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
<img src="<%= post.user.small_avatar_url -%>" style="border-radius:50%;clear:both;display:block;height:50px;width:50px;outline:0;">
</td>
<td style="color:#0a0a0a;line-height:1.3;padding:0 8px 8px 8px;vertical-align:top;">
<h6 style="color:inherit;font-size:16px;font-weight:400;margin:0;padding:0;text-align:left;word-wrap:normal;"><%= post.user.username -%></h6>
<p style="color:#8f8f8f;font-size:16px;line-height:1.3;margin:0;padding:0;text-align:left;"><%= post.user.name -%></p>
<% if post.user %>
<h6 style="color:inherit;font-size:16px;font-weight:400;margin:0;padding:0;text-align:left;word-wrap:normal;"><%= post.user.username -%></h6>
<% if SiteSetting.enable_names? && post.user.name && post.user.name.downcase != post.user.username %>
<p style="color:#8f8f8f;font-size:16px;line-height:1.3;margin:0;padding:0;text-align:left;"><%= post.user.name -%></p>
<% end %>
<% end %>
</td>
<td style="color:#0a0a0a;line-height:1.3;padding:0 8px 8px 8px;text-align:right;">
<p style="color:#8f8f8f;line-height:1.3;margin:0 0 10px 0;padding:0;text-align:right;">
@ -279,23 +285,33 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
<div style="background-color:#f3f3f3">
<table class="spacer" style="padding:0;text-align:left;vertical-align:top;width:100%">
<tbody><tr><td height="40px" style="-moz-hyphens:auto;-webkit-hyphens:auto;border-collapse:collapse!important;color:#0a0a0a;font-size:40px;font-weight:400;hyphens:auto;line-height:40px;margin:0;mso-line-height-rule:exactly;padding:0;text-align:left;vertical-align:top;word-wrap:normal">&#xA0;</td></tr></tbody>
<tbody><tr><td height="40" style="-moz-hyphens:auto;-webkit-hyphens:auto;border-collapse:collapse!important;color:#0a0a0a;font-size:40px;font-weight:400;hyphens:auto;line-height:40px;margin:0;mso-line-height-rule:exactly;padding:0;text-align:left;vertical-align:top;word-wrap:normal">&#xA0;</td></tr></tbody>
</table>
</div>
<!-- End of Popular Post -->
<% end %>
</td>
<td class="side-spacer" style="width:5%;padding:0;">&nbsp;</td>
</tr>
</table>
<% end %>
<% if @other_new_for_you.present? %>
<center style="color:#0a0a0a;font-size:28px;font-weight:400;margin-bottom: 8px;"><%=t 'user_notifications.digest.more_new' %></center>
<center style="color:#0a0a0a;font-size:28px;font-weight:400;margin-bottom: 8px;font-family:Helvetica,Arial,sans-serif;"><%=t 'user_notifications.digest.more_new' %></center>
<%= digest_custom_html("above_popular_topics") %>
<table class="body" style="width:100%;background:#f3f3f3;border-spacing:0;border-collapse:collapse!important;font-family:Helvetica,Arial,sans-serif;font-size:16px;font-weight:200;line-height:1.3;padding:0;text-align:left;vertical-align:top;">
<tr>
<td class="side-spacer" style="width:5%;padding:0;">&nbsp;</td>
<td align="center" valign="top" style="width:90%;border-collapse:collapse!important;margin:0;padding:0;">
<table style="padding:0;text-align:left;vertical-align:top;width:100%">
<tbody>
@ -342,6 +358,11 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo
</tbody>
</table>
</td>
<td class="side-spacer" style="width:5%;padding:0;">&nbsp;</td>
</tr>
</table>
<%= digest_custom_html("below_popular_topics") %>
<% end %>

View File

@ -87,5 +87,6 @@ RailsMultisite::ConnectionManagement.each_connection do
end
if Rails.configuration.multisite
Rails.logger.instance_variable_get(:@chained).first.formatter = RailsMultisite::Formatter.new
chained = Rails.logger.instance_variable_get(:@chained)
chained && chained.first.formatter = RailsMultisite::Formatter.new
end

View File

@ -1286,8 +1286,9 @@ en:
allow_animated_thumbnails: "Generates animated thumbnails of animated gifs."
default_avatars: "URLs to avatars that will be used by default for new users until they change them."
automatically_download_gravatars: "Download Gravatars for users upon account creation or email change."
digest_topics: "The maximum number of topics to display in the email summary."
digest_topics: "The maximum number of popular topics to display in the email summary."
digest_posts: "The maximum number of popular posts to display in the email summary."
digest_other_topics: "The maximum number of topics to show in the 'New in topics and categories you follow' section of the email summary."
digest_min_excerpt_length: "Minimum post excerpt in the email summary, in characters."
delete_digest_email_after_days: "Suppress summary emails for users not seen on the site for more than (n) days."
digest_suppress_categories: "Suppress these categories from summary emails."

View File

@ -588,6 +588,7 @@ email:
default: 5
min: 1
digest_posts: 3
digest_other_topics: 5
delete_digest_email_after_days: 365
digest_suppress_categories:
type: category_list
@ -669,7 +670,7 @@ email:
reset_bounce_score_after_days: 30
attachment_content_type_blacklist:
type: list
default: "pkcs7"
default: "pkcs7|x-vcard"
attachment_filename_blacklist:
type: list
default: "smime.p7s|signature.asc"

View File

@ -6,23 +6,42 @@ class PostgreSQLFallbackHandler
include Singleton
def initialize
@master = {}
@running = {}
@mutex = {}
@last_check = {}
setup!
@masters_down = {}
@mutex = Mutex.new
end
def verify_master
@mutex[namespace].synchronize do
return if running || recently_checked?
@running[namespace] = true
end
synchronize { return if @thread && @thread.alive? }
current_namespace = namespace
Thread.new do
RailsMultisite::ConnectionManagement.with_connection(current_namespace) do
@thread = Thread.new do
while true do
begin
thread = Thread.new { initiate_fallback_to_master }
thread.join
break if synchronize { @masters_down.empty? }
sleep 10
ensure
thread.kill
end
end
end
end
def master_down?
synchronize { @masters_down[namespace] }
end
def master_down=(args)
synchronize { @masters_down[namespace] = args }
end
def master_up(namespace)
synchronize { @masters_down.delete(namespace) }
end
def initiate_fallback_to_master
@masters_down.keys.each do |key|
RailsMultisite::ConnectionManagement.with_connection(key) do
begin
logger.warn "#{log_prefix}: Checking master server..."
connection = ActiveRecord::Base.postgresql_connection(config)
@ -32,54 +51,19 @@ class PostgreSQLFallbackHandler
ActiveRecord::Base.clear_all_connections!
logger.warn "#{log_prefix}: Master server is active. Reconnecting..."
if namespace == RailsMultisite::ConnectionManagement::DEFAULT
ActiveRecord::Base.establish_connection(config)
else
RailsMultisite::ConnectionManagement.establish_connection(db: namespace)
end
self.master_up(key)
Discourse.disable_readonly_mode
self.master = true
end
rescue => e
if e.message.include?("could not connect to server")
logger.warn "#{log_prefix}: Connection to master PostgreSQL server failed with '#{e.message}'"
else
raise e
end
ensure
@mutex[namespace].synchronize do
@last_check[namespace] = Time.zone.now
@running[namespace] = false
end
logger.warn "#{log_prefix}: Connection to master PostgreSQL server failed with '#{e.message}'"
end
end
end
end
def master
@master[namespace]
end
def master=(args)
@master[namespace] = args
end
def running
@running[namespace]
end
# Use for testing
def setup!
RailsMultisite::ConnectionManagement.all_dbs.each do |db|
@master[db] = true
@running[db] = false
@mutex[db] = Mutex.new
@last_check[db] = nil
end
end
def verify?
!master && !running
@masters_down = {}
end
private
@ -96,17 +80,13 @@ class PostgreSQLFallbackHandler
"#{self.class} [#{namespace}]"
end
def recently_checked?
if @last_check[namespace]
Time.zone.now <= (@last_check[namespace] + 5.seconds)
else
false
end
end
def namespace
RailsMultisite::ConnectionManagement.current_db
end
def synchronize
@mutex.synchronize { yield }
end
end
module ActiveRecord
@ -115,7 +95,9 @@ module ActiveRecord
fallback_handler = ::PostgreSQLFallbackHandler.instance
config = config.symbolize_keys
if fallback_handler.verify?
if fallback_handler.master_down?
fallback_handler.verify_master
connection = postgresql_connection(config.dup.merge({
host: config[:replica_host], port: config[:replica_port]
}))
@ -126,7 +108,8 @@ module ActiveRecord
begin
connection = postgresql_connection(config)
rescue PG::ConnectionBad => e
fallback_handler.master = false
fallback_handler.master_down = true
fallback_handler.verify_master
raise e
end
end
@ -141,20 +124,4 @@ module ActiveRecord
raise "Replica database server is not in recovery mode." if value == 'f'
end
end
module ConnectionAdapters
class PostgreSQLAdapter
set_callback :checkout, :before, :switch_back?
private
def fallback_handler
@fallback_handler ||= ::PostgreSQLFallbackHandler.instance
end
def switch_back?
fallback_handler.verify_master if fallback_handler.verify?
end
end
end
end

View File

@ -228,10 +228,12 @@ module Discourse
def self.keep_readonly_mode
# extend the expiry by 1 minute every 30 seconds
Thread.new do
while readonly_mode?
$redis.expire(READONLY_MODE_KEY, READONLY_MODE_KEY_TTL)
sleep 30.seconds
unless Rails.env.test?
Thread.new do
while readonly_mode?
$redis.expire(READONLY_MODE_KEY, READONLY_MODE_KEY_TTL)
sleep 30.seconds
end
end
end
end

View File

@ -244,14 +244,21 @@ module Email
address_field.decoded
from_address = address_field.address
from_display_name = address_field.display_name.try(:to_s)
return [from_address.downcase, from_display_name] if from_address["@"]
return [from_address&.downcase, from_display_name&.strip] if from_address["@"]
end
end
from_address = mail.from[/<([^>]+)>/, 1]
from_display_name = mail.from[/^([^<]+)/, 1]
if mail.from[/<[^>]+>/]
from_address = mail.from[/<([^>]+)>/, 1]
from_display_name = mail.from[/^([^<]+)/, 1]
end
[from_address.downcase, from_display_name]
if (from_address.blank? || !from_address["@"]) && mail.from[/\[mailto:[^\]]+\]/]
from_address = mail.from[/\[mailto:([^\]]+)\]/, 1]
from_display_name = mail.from[/^([^\[]+)/, 1]
end
[from_address&.downcase, from_display_name&.strip]
end
def subject
@ -376,6 +383,9 @@ module Email
def process_forwarded_email(destination, user)
embedded = Mail.new(@embedded_email_raw)
email, display_name = parse_from_field(embedded)
return false if email.blank? || !email["@"]
embedded_user = find_or_create_user(email, display_name)
raw = try_to_encode(embedded.decoded, "UTF-8").presence || embedded.to_s
title = embedded.subject.presence || subject
@ -387,6 +397,7 @@ module Email
raw: raw,
title: title,
archetype: Archetype.private_message,
target_usernames: [user.username],
target_group_names: [group.name],
is_group_message: true,
skip_validations: true,
@ -409,11 +420,14 @@ module Email
end
if post && post.topic && @before_embedded.present?
post_type = Post.types[:regular]
post_type = Post.types[:whisper] if post.topic.private_message? && group.usernames[user.username]
create_reply(user: user,
raw: @before_embedded,
post: post,
topic: post.topic,
post_type: Post.types[:whisper])
post_type: post_type)
end
true

View File

@ -33,7 +33,7 @@ class SystemMessage
post = creator.create
if creator.errors.present?
raise StandardError, creator.errors.to_s
raise StandardError, creator.errors.full_messages.join(" ")
end
UserArchivedMessage.create!(user: Discourse.site_contact_user, topic: post.topic)

View File

@ -1,6 +1,6 @@
desc "Runs the qunit test suite"
task "qunit:test" => :environment do
task "qunit:test", [:timeout] => :environment do |_, args|
require "rack"
require "socket"
@ -35,7 +35,7 @@ task "qunit:test" => :environment do
begin
success = true
test_path = "#{Rails.root}/vendor/assets/javascripts"
cmd = "phantomjs #{test_path}/run-qunit.js http://localhost:#{port}/qunit"
cmd = "phantomjs #{test_path}/run-qunit.js http://localhost:#{port}/qunit #{args[:timeout]}"
options = {}

View File

@ -89,7 +89,7 @@ function initializePolls(api) {
);
$poll.replaceWith($div);
Em.run.schedule('afterRender', () => pollComponent.renderer.replaceIn(pollComponent, $div[0]));
Em.run.schedule('afterRender', () => pollComponent.renderer.appendTo(pollComponent, $div[0]));
postPollViews[pollId] = pollComponent;
});

File diff suppressed because one or more lines are too long

View File

@ -48,8 +48,9 @@ describe ActiveRecord::ConnectionHandling do
end
it 'should failover to a replica server' do
current_threads = Thread.list
RailsMultisite::ConnectionManagement.stubs(:all_dbs).returns(['default', multisite_db])
::PostgreSQLFallbackHandler.instance.setup!
[config, multisite_config].each do |configuration|
ActiveRecord::Base.expects(:postgresql_connection).with(configuration).raises(PG::ConnectionBad)
@ -60,7 +61,7 @@ describe ActiveRecord::ConnectionHandling do
})).returns(@replica_connection)
end
expect(postgresql_fallback_handler.master).to eq(true)
expect(postgresql_fallback_handler.master_down?).to eq(nil)
expect { ActiveRecord::Base.postgresql_fallback_connection(config) }
.to raise_error(PG::ConnectionBad)
@ -68,10 +69,10 @@ describe ActiveRecord::ConnectionHandling do
expect{ ActiveRecord::Base.postgresql_fallback_connection(config) }
.to change{ Discourse.readonly_mode? }.from(false).to(true)
expect(postgresql_fallback_handler.master).to eq(false)
expect(postgresql_fallback_handler.master_down?).to eq(true)
with_multisite_db(multisite_db) do
expect(postgresql_fallback_handler.master).to eq(true)
expect(postgresql_fallback_handler.master_down?).to eq(nil)
expect { ActiveRecord::Base.postgresql_fallback_connection(multisite_config) }
.to raise_error(PG::ConnectionBad)
@ -79,30 +80,18 @@ describe ActiveRecord::ConnectionHandling do
expect{ ActiveRecord::Base.postgresql_fallback_connection(multisite_config) }
.to change{ Discourse.readonly_mode? }.from(false).to(true)
expect(postgresql_fallback_handler.master).to eq(false)
expect(postgresql_fallback_handler.master_down?).to eq(true)
end
postgresql_fallback_handler.master_up(multisite_db)
ActiveRecord::Base.unstub(:postgresql_connection)
current_threads = Thread.list
expect{ ActiveRecord::Base.connection_pool.checkout }
.to change{ Thread.list.size }.by(1)
# Ensure that we don't try to connect back to the replica when a thread
# is running
begin
ActiveRecord::Base.postgresql_fallback_connection(config)
rescue PG::ConnectionBad => e
# This is expected if the thread finishes before the above is called.
end
# Wait for the thread to finish execution
(Thread.list - current_threads).each(&:join)
postgresql_fallback_handler.initiate_fallback_to_master
expect(Discourse.readonly_mode?).to eq(false)
expect(PostgreSQLFallbackHandler.instance.master).to eq(true)
expect(postgresql_fallback_handler.master_down?).to eq(nil)
expect(ActiveRecord::Base.connection_pool.connections.count).to eq(0)

View File

@ -383,6 +383,39 @@ describe Email::Receiver do
expect(Post.last.raw).to match(/discourse\.rb/)
end
it "handles forwarded emails" do
SiteSetting.enable_forwarded_emails = true
expect { process(:forwarded_email_1) }.to change(Topic, :count)
forwarded_post, last_post = *Post.last(2)
expect(forwarded_post.user.email).to eq("some@one.com")
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[:regular])
end
it "handles weirdly forwarded emails" do
group.add(Fabricate(:user, email: "ba@bar.com"))
group.save
SiteSetting.enable_forwarded_emails = true
expect { process(:forwarded_email_2) }.to change(Topic, :count)
forwarded_post, last_post = *Post.last(2)
expect(forwarded_post.user.email).to eq("some@one.com")
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
end
context "new topic in a category" do

Binary file not shown.

Binary file not shown.

View File

@ -155,8 +155,7 @@ describe UserNotifications do
context "with new topics" do
before do
Topic.stubs(:for_digest).returns([Fabricate(:topic, user: Fabricate(:coding_horror))])
Topic.stubs(:new_since_last_seen).returns(Topic.none)
Fabricate(:topic, user: Fabricate(:coding_horror))
end
it "works" do
@ -184,6 +183,28 @@ describe UserNotifications do
expect(html).to_not include deleted.title
expect(html).to_not include post.raw
end
it "excludes whispers and other post types that don't belong" do
t = Fabricate(:topic, user: Fabricate(:user), title: "Who likes the same stuff I like?")
whisper = Fabricate(:post, topic: t, score: 100.0, post_number: 2, raw: "You like weird stuff", post_type: Post.types[:whisper])
mod_action = Fabricate(:post, topic: t, score: 100.0, post_number: 3, raw: "This topic unlisted", post_type: Post.types[:moderator_action])
small_action = Fabricate(:post, topic: t, score: 100.0, post_number: 4, raw: "A small action", post_type: Post.types[:small_action])
html = subject.html_part.body.to_s
expect(html).to_not include whisper.raw
expect(html).to_not include mod_action.raw
expect(html).to_not include small_action.raw
end
it "excludes deleted and hidden posts" do
t = Fabricate(:topic, user: Fabricate(:user), title: "Post objectionable stuff here")
deleted = Fabricate(:post, topic: t, score: 100.0, post_number: 2, raw: "This post is uncalled for", deleted_at: 5.minutes.ago)
hidden = Fabricate(:post, topic: t, score: 100.0, post_number: 3, raw: "Try to find this post", hidden: true, hidden_at: 5.minutes.ago, hidden_reason_id: Post.hidden_reasons[:flagged_by_tl3_user])
user_deleted = Fabricate(:post, topic: t, score: 100.0, post_number: 4, raw: "I regret this post", user_deleted: true)
html = subject.html_part.body.to_s
expect(html).to_not include deleted.raw
expect(html).to_not include hidden.raw
expect(html).to_not include user_deleted.raw
end
end
end

View File

@ -15,9 +15,15 @@ describe QuotedPost do
post1 = Fabricate(:post)
post2 = Fabricate(:post)
post2.cooked = <<HTML
<aside class="quote" data-post="#{post1.post_number}" data-topic="#{post1.topic_id}"><div class="title"><div class="quote-controls"></div><img width="20" height="20" src="/user_avatar/meta.discourse.org/techapj/20/3281.png" class="avatar">techAPJ:</div><blockquote><p>When the user will v</p></blockquote></aside>
HTML
post2.cooked = <<-HTML
<aside class="quote" data-post="#{post1.post_number}" data-topic="#{post1.topic_id}">
<div class="title">
<div class="quote-controls"></div>
<img width="20" height="20" src="/user_avatar/meta.discourse.org/techapj/20/3281.png" class="avatar">techAPJ:
</div>
<blockquote><p>When the user will v</p></blockquote>
</aside>
HTML
QuotedPost.create!(post_id: post2.id, quoted_post_id: 999)

View File

@ -36,7 +36,7 @@ page.open(args[0], function(status) {
} else {
page.evaluate(logQUnit);
var timeout = parseInt(args[1] || 130000, 10),
var timeout = parseInt(args[1] || 200000, 10),
start = Date.now();
var interval = setInterval(function() {