Merge branch 'master' into fix_whisper
This commit is contained in:
commit
9b885c039a
|
@ -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']"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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') });
|
||||
|
|
|
@ -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: '/' });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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"}}
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -20,7 +20,7 @@ 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
|
||||
|
@ -33,9 +33,9 @@ class QuotedPost < ActiveRecord::Base
|
|||
", 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
|
||||
ids << results[0]["quoted_post_id"].to_i if results.length > 0
|
||||
rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation
|
||||
# it's fine
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -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,11 +163,8 @@ SQL
|
|||
|
||||
added_urls << url
|
||||
|
||||
topic_link = TopicLink.find_by(topic_id: post.topic_id,
|
||||
post_id: post.id,
|
||||
url: url)
|
||||
|
||||
unless topic_link
|
||||
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,
|
||||
|
@ -177,6 +174,9 @@ SQL
|
|||
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
|
||||
|
||||
|
|
|
@ -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? %>
|
||||
<% 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;">
|
||||
<% 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"> </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"> </td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- End of Popular Post -->
|
||||
|
||||
<% end %>
|
||||
|
||||
</td>
|
||||
<td class="side-spacer" style="width:5%;padding:0;"> </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;"> </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;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<%= digest_custom_html("below_popular_topics") %>
|
||||
|
||||
<% end %>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
synchronize { return if @thread && @thread.alive? }
|
||||
|
||||
@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
|
||||
|
||||
current_namespace = namespace
|
||||
Thread.new do
|
||||
RailsMultisite::ConnectionManagement.with_connection(current_namespace) do
|
||||
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
|
||||
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
|
||||
|
|
|
@ -228,6 +228,7 @@ module Discourse
|
|||
|
||||
def self.keep_readonly_mode
|
||||
# extend the expiry by 1 minute every 30 seconds
|
||||
unless Rails.env.test?
|
||||
Thread.new do
|
||||
while readonly_mode?
|
||||
$redis.expire(READONLY_MODE_KEY, READONLY_MODE_KEY_TTL)
|
||||
|
@ -235,6 +236,7 @@ module Discourse
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.disable_readonly_mode(user_enabled: false)
|
||||
key = user_enabled ? USER_READONLY_MODE_KEY : READONLY_MODE_KEY
|
||||
|
|
|
@ -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
|
||||
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {}
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
|
File diff suppressed because one or more lines are too long
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
Message-ID: <58@foo.bar.mail>
|
||||
From: Ba Bar <ba@bar.com>
|
||||
To: Team <team@bar.com>
|
||||
Date: Mon, 1 Dec 2016 13:37:42 +0100
|
||||
Subject: FW: Discoursing much?
|
||||
|
||||
@team, can you have a look at this email below?
|
||||
|
||||
From: Some One <some@one.com>
|
||||
To: Ba Bar <ba@bar.com>
|
||||
Date: Mon, 1 Dec 2016 00:13:37 +0100
|
||||
Subject: Discoursing much?
|
||||
|
||||
Hello Ba Bar,
|
||||
|
||||
Discoursing much today?
|
||||
|
||||
XoXo
|
|
@ -0,0 +1,18 @@
|
|||
Message-ID: <59@foo.bar.mail>
|
||||
From: Ba Bar <ba@bar.com>
|
||||
To: Team <team@bar.com>
|
||||
Date: Mon, 1 Dec 2016 13:37:42 +0100
|
||||
Subject: Re: Discoursing much?
|
||||
|
||||
@team, can you have a look at this email below?
|
||||
|
||||
From: Some One [mailto:some@one.com]
|
||||
To: Ba Bar <ba@bar.com>
|
||||
Date: Mon, 1 Dec 2016 00:13:37 +0100
|
||||
Subject: Discoursing much?
|
||||
|
||||
Hello Ba Bar,
|
||||
|
||||
Discoursing much today?
|
||||
|
||||
XoXo
|
|
@ -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
|
||||
|
|
|
@ -15,8 +15,14 @@ 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>
|
||||
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)
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue