Version bump

This commit is contained in:
Roman Rizzi 2019-03-13 16:47:46 -03:00
commit 1234acd2dd
127 changed files with 1186 additions and 471 deletions

2
.rspec
View File

@ -1,3 +1 @@
--colour
--profile
--fail-fast

14
Gemfile
View File

@ -14,13 +14,13 @@ if rails_master?
else
# until rubygems gives us optional dependencies we are stuck with this
# bundle update actionmailer actionpack actionview activemodel activerecord activesupport railties
gem 'actionmailer', '5.2.2'
gem 'actionpack', '5.2.2'
gem 'actionview', '5.2.2'
gem 'activemodel', '5.2.2'
gem 'activerecord', '5.2.2'
gem 'activesupport', '5.2.2'
gem 'railties', '5.2.2'
gem 'actionmailer', '5.2.2.1'
gem 'actionpack', '5.2.2.1'
gem 'actionview', '5.2.2.1'
gem 'activemodel', '5.2.2.1'
gem 'activerecord', '5.2.2.1'
gem 'activesupport', '5.2.2.1'
gem 'railties', '5.2.2.1'
gem 'sprockets-rails'
end

View File

@ -1,37 +1,37 @@
GEM
remote: https://rubygems.org/
specs:
actionmailer (5.2.2)
actionpack (= 5.2.2)
actionview (= 5.2.2)
activejob (= 5.2.2)
actionmailer (5.2.2.1)
actionpack (= 5.2.2.1)
actionview (= 5.2.2.1)
activejob (= 5.2.2.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.2.2)
actionview (= 5.2.2)
activesupport (= 5.2.2)
actionpack (5.2.2.1)
actionview (= 5.2.2.1)
activesupport (= 5.2.2.1)
rack (~> 2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.2)
activesupport (= 5.2.2)
actionview (5.2.2.1)
activesupport (= 5.2.2.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_model_serializers (0.8.4)
activemodel (>= 3.0)
activejob (5.2.2)
activesupport (= 5.2.2)
activejob (5.2.2.1)
activesupport (= 5.2.2.1)
globalid (>= 0.3.6)
activemodel (5.2.2)
activesupport (= 5.2.2)
activerecord (5.2.2)
activemodel (= 5.2.2)
activesupport (= 5.2.2)
activemodel (5.2.2.1)
activesupport (= 5.2.2.1)
activerecord (5.2.2.1)
activemodel (= 5.2.2.1)
activesupport (= 5.2.2.1)
arel (>= 9.0)
activesupport (5.2.2)
activesupport (5.2.2.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@ -308,9 +308,9 @@ GEM
rails_multisite (2.0.6)
activerecord (> 4.2, < 6)
railties (> 4.2, < 6)
railties (5.2.2)
actionpack (= 5.2.2)
activesupport (= 5.2.2)
railties (5.2.2.1)
actionpack (= 5.2.2.1)
activesupport (= 5.2.2.1)
method_source
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
@ -446,13 +446,13 @@ PLATFORMS
ruby
DEPENDENCIES
actionmailer (= 5.2.2)
actionpack (= 5.2.2)
actionview (= 5.2.2)
actionmailer (= 5.2.2.1)
actionpack (= 5.2.2.1)
actionview (= 5.2.2.1)
active_model_serializers (~> 0.8.3)
activemodel (= 5.2.2)
activerecord (= 5.2.2)
activesupport (= 5.2.2)
activemodel (= 5.2.2.1)
activerecord (= 5.2.2.1)
activesupport (= 5.2.2.1)
annotate
aws-sdk-s3
aws-sdk-sns
@ -525,7 +525,7 @@ DEPENDENCIES
rack-mini-profiler
rack-protection
rails_multisite
railties (= 5.2.2)
railties (= 5.2.2.1)
rake
rb-fsevent
rb-inotify (~> 0.9)

View File

@ -376,24 +376,21 @@ export default Ember.Component.extend({
_applyCategoryHashtagAutocomplete() {
const siteSettings = this.siteSettings;
const self = this;
this.$(".d-editor-input").autocomplete({
template: findRawTemplate("category-tag-autocomplete"),
key: "#",
afterComplete() {
self._focusTextArea();
},
transformComplete(obj) {
afterComplete: () => this._focusTextArea(),
transformComplete: obj => {
return obj.text;
},
dataSource(term) {
dataSource: term => {
if (term.match(/\s/)) {
return null;
}
return searchCategoryTag(term, siteSettings);
},
triggerRule(textarea, opts) {
triggerRule: (textarea, opts) => {
return categoryHashtagTriggerRule(textarea, opts);
}
});
@ -404,17 +401,15 @@ export default Ember.Component.extend({
return;
}
const self = this;
$editorInput.autocomplete({
template: findRawTemplate("emoji-selector-autocomplete"),
key: ":",
afterComplete(text) {
self.set("value", text);
self._focusTextArea();
afterComplete: text => {
this.set("value", text);
this._focusTextArea();
},
onKeyUp(text, cp) {
onKeyUp: (text, cp) => {
const matches = /(?:^|[^a-z])(:(?!:).?[\w-]*:?(?!:)(?:t\d?)?:?) ?$/gi.exec(
text.substring(0, cp)
);
@ -424,22 +419,26 @@ export default Ember.Component.extend({
}
},
transformComplete(v) {
transformComplete: v => {
if (v.code) {
return `${v.code}:`;
} else {
$editorInput.autocomplete({ cancel: true });
self.set("emojiPickerIsActive", true);
this.set(
"isEditorFocused",
$("textarea.d-editor-input").is(":focus")
);
this.set("emojiPickerIsActive", true);
return "";
}
},
dataSource(term) {
dataSource: term => {
return new Ember.RSVP.Promise(resolve => {
const full = `:${term}`;
term = term.toLowerCase();
if (term.length < self.siteSettings.emoji_autocomplete_min_chars) {
if (term.length < this.siteSettings.emoji_autocomplete_min_chars) {
return resolve([]);
}
@ -856,6 +855,7 @@ export default Ember.Component.extend({
return;
}
this.set("isEditorFocused", $("textarea.d-editor-input").is(":focus"));
this.set("emojiPickerIsActive", !this.get("emojiPickerIsActive"));
},

View File

@ -2,6 +2,11 @@ import { setting } from "discourse/lib/computed";
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
import computed from "ember-addons/ember-computed-decorators";
const categorySortCriteria = [];
export function addCategorySortCriteria(criteria) {
categorySortCriteria.push(criteria);
}
export default buildCategoryPanel("settings", {
emailInEnabled: setting("email_in"),
showPositionInput: setting("fixed_category_positions"),
@ -64,10 +69,9 @@ export default buildCategoryPanel("settings", {
"category",
"created"
]
.concat(categorySortCriteria)
.map(s => ({ name: I18n.t("category.sort_options." + s), value: s }))
.sort((a, b) => {
return a.name > b.name;
});
.sort((a, b) => a.name.localeCompare(b.name));
},
@computed

View File

@ -7,6 +7,7 @@ import {
isSkinTonableEmoji,
emojiSearch
} from "pretty-text/emoji";
import { safariHacksDisabled } from "discourse/lib/utilities";
const { run } = Ember;
const keyValueStore = new KeyValueStore("discourse_emojis_");
@ -58,6 +59,12 @@ export default Ember.Component.extend({
this._scrollTo();
this._updateSelectedDiversity();
this._checkVisibleSection(true);
if (
(!this.site.isMobileDevice || this.get("isEditorFocused")) &&
!safariHacksDisabled()
)
this.$filter.find("input[name='filter']").focus();
});
},

View File

@ -32,9 +32,9 @@ export default Ember.Component.extend({
"emailOrUsername",
"invitingToTopic",
"isPrivateTopic",
"topic.groupNames",
"topic.saving",
"topic.details.can_invite_to"
"inviteModel.groupNames.[]",
"inviteModel.saving",
"inviteModel.details.can_invite_to"
)
disabled(
isAdmin,
@ -79,7 +79,7 @@ export default Ember.Component.extend({
"emailOrUsername",
"inviteModel.saving",
"isPrivateTopic",
"inviteModel.groupNames",
"inviteModel.groupNames.[]",
"hasCustomMessage"
)
disabledCopyLink(

View File

@ -1,6 +1,7 @@
import { iconHTML } from "discourse-common/lib/icon-library";
import { bufferedRender } from "discourse-common/lib/buffered-render";
import { escapeExpression } from "discourse/lib/utilities";
import TopicStatusIcons from "discourse/helpers/topic-status-icons";
export default Ember.Component.extend(
bufferedRender({
@ -30,7 +31,15 @@ export default Ember.Component.extend(
}.property("disableActions"),
buildBuffer(buffer) {
const renderIcon = function(name, key, actionable) {
const canAct = this.get("canAct");
const topic = this.get("topic");
if (!topic) {
return;
}
TopicStatusIcons.render(topic, function(name, key) {
const actionable = ["pinned", "unpinned"].includes(key) && canAct;
const title = escapeExpression(I18n.t(`topic_statuses.${key}.help`)),
startTag = actionable ? "a href" : "span",
endTag = actionable ? "a" : "span",
@ -40,32 +49,7 @@ export default Ember.Component.extend(
buffer.push(
`<${startTag} title='${title}' class='topic-status'>${icon}</${endTag}>`
);
};
const renderIconIf = (conditionProp, name, key, actionable) => {
if (!this.get(conditionProp)) {
return;
}
renderIcon(name, key, actionable);
};
renderIconIf("topic.is_warning", "envelope", "warning");
if (this.get("topic.closed") && this.get("topic.archived")) {
renderIcon("lock", "locked_and_archived");
} else {
renderIconIf("topic.closed", "lock", "locked");
renderIconIf("topic.archived", "lock", "archived");
}
renderIconIf("topic.pinned", "thumbtack", "pinned", this.get("canAct"));
renderIconIf(
"topic.unpinned",
"thumbtack",
"unpinned",
this.get("canAct")
);
renderIconIf("topic.invisible", "far-eye-slash", "unlisted");
});
}
})
);

View File

@ -74,9 +74,25 @@ export default Ember.Controller.extend(BulkTopicSelection, {
return listDraft ? "topic.open_draft" : "topic.create";
},
@computed("canCreateTopic", "category", "canCreateTopicOnCategory")
createTopicDisabled(canCreateTopic, category, canCreateTopicOnCategory) {
return !canCreateTopic || (category && !canCreateTopicOnCategory);
@computed(
"canCreateTopic",
"category",
"canCreateTopicOnCategory",
"tag",
"canCreateTopicOnTag"
)
createTopicDisabled(
canCreateTopic,
category,
canCreateTopicOnCategory,
tag,
canCreateTopicOnTag
) {
return (
!canCreateTopic ||
(category && !canCreateTopicOnCategory) ||
(tag && !canCreateTopicOnTag)
);
},
queryParams: [

View File

@ -0,0 +1,26 @@
export default Ember.ArrayProxy.extend({
render(topic, renderIcon) {
const renderIconIf = (conditionProp, name, key) => {
if (!topic.get(conditionProp)) {
return;
}
renderIcon(name, key);
};
if (topic.get("closed") && topic.get("archived")) {
renderIcon("lock", "locked_and_archived");
} else {
renderIconIf("closed", "lock", "locked");
renderIconIf("archived", "lock", "archived");
}
this.forEach(args => renderIconIf(...args));
}
}).create({
content: [
["is_warning", "envelope", "warning"],
["pinned", "thumbtack", "pinned"],
["unpinned", "thumbtack", "unpinned"],
["invisible", "far-eye-slash", "unlisted"]
]
});

View File

@ -41,9 +41,10 @@ import { disableNameSuppression } from "discourse/widgets/poster-name";
import { registerCustomPostMessageCallback as registerCustomPostMessageCallback1 } from "discourse/controllers/topic";
import Sharing from "discourse/lib/sharing";
import { addComposerUploadHandler } from "discourse/components/composer-editor";
import { addCategorySortCriteria } from "discourse/components/edit-category-settings";
// If you add any methods to the API ensure you bump up this number
const PLUGIN_API_VERSION = "0.8.29";
const PLUGIN_API_VERSION = "0.8.30";
class PluginApi {
constructor(version, container) {
@ -801,7 +802,6 @@ class PluginApi {
}
/**
*
* Registers a function to handle uploads for specified file types
* The normal uploading functionality will be bypassed if function returns
* a falsy value.
@ -817,6 +817,18 @@ class PluginApi {
addComposerUploadHandler(extensions, method);
}
/**
* Registers a criteria that can be used as default topic order on category
* pages.
*
* Example:
*
* categorySortCriteria("votes");
*/
addCategorySortCriteria(criteria) {
addCategorySortCriteria(criteria);
}
/**
* Registers a renderer that overrides the display of category links.
*

View File

@ -111,14 +111,18 @@ export default Discourse.Route.extend({
).then(list => {
if (list.topic_list.tags && list.topic_list.tags.length === 1) {
// Update name of tag (case might be different)
tag.set("id", list.topic_list.tags[0].name);
tag.setProperties({
id: list.topic_list.tags[0].name,
staff: list.topic_list.tags[0].staff
});
}
controller.setProperties({
list,
canCreateTopic: list.get("can_create_topic"),
loading: false,
canCreateTopicOnCategory:
this.get("category.permission") === PermissionType.FULL
this.get("category.permission") === PermissionType.FULL,
canCreateTopicOnTag: !tag.get("staff") || this.get("currentUser.staff")
});
});
},

View File

@ -50,4 +50,4 @@
</div>
</div>
{{emoji-picker active=emojiPickerIsActive emojiSelected=(action 'emojiSelected')}}
{{emoji-picker active=emojiPickerIsActive isEditorFocused=isEditorFocused emojiSelected=(action 'emojiSelected')}}

View File

@ -85,9 +85,9 @@
{{i18n "category.sort_order"}}
</label>
<div class="controls">
{{combo-box valueAttribute="value" id="category-sort-order" content=availableSorts value=category.sort_order none="category.sort_options.default"}}
{{combo-box valueAttribute="value" content=availableSorts value=category.sort_order none="category.sort_options.default"}}
{{#unless isDefaultSortOrder}}
{{combo-box castBoolean=true valueAttribute="value" id="category-sort-order" content=sortAscendingOptions value=category.sort_ascending none="category.sort_options.default"}}
{{combo-box castBoolean=true valueAttribute="value" content=sortAscendingOptions value=category.sort_ascending none="category.sort_options.default"}}
{{/unless}}
</div>
</section>

View File

@ -1,5 +1,14 @@
<label class="control-label" for="{{concat 'user-' elementId}}">{{{field.name}}} {{#if field.required}}<span class='required'>*</span>{{/if}}
</label>
{{#if field.name}}
<label class="control-label" for="{{concat 'user-' elementId}}">
{{{field.name}}} {{#if field.required}}<span class='required'>*</span>{{/if}}
</label>
{{/if}}
<div class='controls'>
<label class="control-label checkbox-label">{{input id=(concat 'user-' elementId) checked=value type="checkbox"}} <span>{{{field.description}}}</span></label>
<label class="control-label checkbox-label">
{{input id=(concat 'user-' elementId) checked=value type="checkbox"}}
<span>
{{{field.description}}} {{#unless field.name}}{{#if field.required}}<span class='required'>*</span>{{/if}}{{/unless}}
</span>
</label>
</div>

View File

@ -57,7 +57,7 @@
icon="far-eye"
label="user.unignore"}}
{{else}}
{{d-button class="btn-danger"
{{d-button class="btn-default"
action=(action "ignoreUser")
icon="eye-slash"
label="user.ignore"}}

View File

@ -17,7 +17,7 @@ export default createWidget("home-logo", {
},
logoUrl() {
return this.siteSettings.site_home_logo_url || "";
return this.siteSettings.site_logo_url || "";
},
mobileLogoUrl() {

View File

@ -431,15 +431,28 @@ createWidget("post-contents", {
createWidget("post-notice", {
tagName: "div.post-notice",
buildClasses(attrs) {
if (attrs.postNoticeType === "first") {
return ["new-user"];
} else if (attrs.postNoticeType === "returning") {
return ["returning-user"];
}
return [];
},
html(attrs) {
const user =
this.siteSettings.prioritize_username_in_ux || !attrs.name
? attrs.username
: attrs.name;
let text, icon;
if (attrs.postNoticeType === "first") {
icon = "hands-helping";
text = I18n.t("post.notice.first", { user: attrs.username });
text = I18n.t("post.notice.first", { user });
} else if (attrs.postNoticeType === "returning") {
icon = "far-smile";
text = I18n.t("post.notice.return", {
user: attrs.username,
user,
time: relativeAge(attrs.postNoticeTime, {
format: "tiny",
addAgo: true

View File

@ -16,6 +16,7 @@ createWidget("admin-menu-button", {
action: attrs.action,
icon: attrs.icon,
label: attrs.fullLabel || `topic.${attrs.label}`,
title: attrs.title,
secondaryAction: "hideAdminMenu"
})
);
@ -136,7 +137,8 @@ export default createWidget("topic-admin-menu", {
buttonClass: "btn-default",
action: "toggleMultiSelect",
icon: "tasks",
label: "actions.multi_select"
label: "actions.multi_select",
title: "topic.actions.multi_select_tooltip"
});
const topic = attrs.topic;
@ -148,7 +150,8 @@ export default createWidget("topic-admin-menu", {
buttonClass: "btn-danger",
action: "deleteTopic",
icon: "far-trash-alt",
label: "actions.delete"
label: "actions.delete",
title: "topic.actions.delete_tooltip"
});
}
@ -158,7 +161,8 @@ export default createWidget("topic-admin-menu", {
buttonClass: "btn-default",
action: "recoverTopic",
icon: "undo",
label: "actions.recover"
label: "actions.recover",
title: "topic.actions.recover_tooltip"
});
}
@ -168,7 +172,8 @@ export default createWidget("topic-admin-menu", {
buttonClass: "btn-default",
action: "toggleClosed",
icon: "unlock",
label: "actions.open"
label: "actions.open",
title: "topic.actions.open_tooltip"
});
} else {
buttons.push({
@ -176,7 +181,8 @@ export default createWidget("topic-admin-menu", {
buttonClass: "btn-default",
action: "toggleClosed",
icon: "lock",
label: "actions.close"
label: "actions.close",
title: "topic.actions.close_tooltip"
});
}
@ -185,7 +191,8 @@ export default createWidget("topic-admin-menu", {
buttonClass: "btn-default",
action: "showTopicStatusUpdate",
icon: "far-clock",
label: "actions.timed_update"
label: "actions.timed_update",
title: "topic.actions.timed_update_tooltip"
});
const isPrivateMessage = topic.get("isPrivateMessage");
@ -197,7 +204,10 @@ export default createWidget("topic-admin-menu", {
buttonClass: "btn-default",
action: "showFeatureTopic",
icon: "thumbtack",
label: featured ? "actions.unpin" : "actions.pin"
label: featured ? "actions.unpin" : "actions.pin",
title: featured
? "topic.actions.unpin_tooltip"
: "topic.actions.pin_tooltip"
});
}
@ -207,7 +217,8 @@ export default createWidget("topic-admin-menu", {
buttonClass: "btn-default",
action: "showChangeTimestamp",
icon: "calendar-alt",
label: "change_timestamp.title"
label: "change_timestamp.title",
title: "topic.change_timestamp.tooltip"
});
}
@ -216,7 +227,8 @@ export default createWidget("topic-admin-menu", {
buttonClass: "btn-default",
action: "resetBumpDate",
icon: "anchor",
label: "actions.reset_bump_date"
label: "actions.reset_bump_date",
title: "topic.actions.reset_bump_date_tooltip"
});
if (!isPrivateMessage) {
@ -225,7 +237,10 @@ export default createWidget("topic-admin-menu", {
buttonClass: "btn-default",
action: "toggleArchived",
icon: "folder",
label: topic.get("archived") ? "actions.unarchive" : "actions.archive"
label: topic.get("archived") ? "actions.unarchive" : "actions.archive",
title: topic.get("archived")
? "topic.actions.unarchive_tooltip"
: "topic.actions.archive_tooltip"
});
}
@ -235,7 +250,10 @@ export default createWidget("topic-admin-menu", {
buttonClass: "btn-default",
action: "toggleVisibility",
icon: visible ? "far-eye-slash" : "far-eye",
label: visible ? "actions.invisible" : "actions.visible"
label: visible ? "actions.invisible" : "actions.visible",
title: visible
? "topic.actions.invisible_tooltip"
: "topic.actions.visible_tooltip"
});
if (details.get("can_convert_topic")) {
@ -246,7 +264,12 @@ export default createWidget("topic-admin-menu", {
? "convertToPublicTopic"
: "convertToPrivateMessage",
icon: isPrivateMessage ? "comment" : "envelope",
label: isPrivateMessage ? "actions.make_public" : "actions.make_private"
label: isPrivateMessage
? "actions.make_public"
: "actions.make_private",
title: isPrivateMessage
? "topic.actions.make_public_tooltip"
: "topic.actions.make_private_tooltip"
});
}
@ -255,7 +278,8 @@ export default createWidget("topic-admin-menu", {
action: "showModerationHistory",
buttonClass: "btn-default",
icon: "list",
fullLabel: "admin.flags.moderation_history"
fullLabel: "admin.flags.moderation_history",
title: "admin.flags.moderation_history_tooltip"
});
}

View File

@ -2,16 +2,7 @@ import { createWidget } from "discourse/widgets/widget";
import { iconNode } from "discourse-common/lib/icon-library";
import { h } from "virtual-dom";
import { escapeExpression } from "discourse/lib/utilities";
function renderIcon(name, key, canAct) {
const iconArgs = key === "unpinned" ? { class: "unpinned" } : null,
icon = iconNode(name, iconArgs);
const attributes = {
title: escapeExpression(I18n.t(`topic_statuses.${key}.help`))
};
return h(`${canAct ? "a" : "span"}.topic-status`, attributes, icon);
}
import TopicStatusIcons from "discourse/helpers/topic-status-icons";
export default createWidget("topic-status", {
tagName: "div.topic-statuses",
@ -21,25 +12,16 @@ export default createWidget("topic-status", {
const canAct = this.currentUser && !attrs.disableActions;
const result = [];
const renderIconIf = (conditionProp, name, key) => {
if (!topic.get(conditionProp)) {
return;
}
result.push(renderIcon(name, key, canAct));
};
renderIconIf("is_warning", "envelope", "warning");
TopicStatusIcons.render(topic, function(name, key) {
const iconArgs = key === "unpinned" ? { class: "unpinned" } : null;
const icon = iconNode(name, iconArgs);
if (topic.get("closed") && topic.get("archived")) {
renderIcon("lock", "locked_and_archived");
} else {
renderIconIf("closed", "lock", "locked");
renderIconIf("archived", "lock", "archived");
}
renderIconIf("pinned", "thumbtack", "pinned");
renderIconIf("unpinned", "thumbtack", "unpinned");
renderIconIf("invisible", "far-eye-slash", "unlisted");
const attributes = {
title: escapeExpression(I18n.t(`topic_statuses.${key}.help`))
};
result.push(h(`${canAct ? "a" : "span"}.topic-status`, attributes, icon));
});
return result;
}

View File

@ -54,6 +54,14 @@
.social-link {
margin-right: s(2);
font-size: $font-up-4;
.d-icon-fab-facebook-square {
// Adheres to Facebook brand guidelines
color: dark-light-choose($facebook, white);
}
.d-icon-fab-twitter-square {
// Adheres to Twitter brand guidelines
color: dark-light-choose($twitter, white);
}
}
.link {
font-size: $font-up-3;

View File

@ -15,7 +15,7 @@ class Admin::SiteSettingsController < Admin::AdminController
raise_access_hidden_setting(id)
if SiteSetting.type_supervisor.get_type(id) == :upload
value = Upload.get_from_url(value) || ''
value = Upload.find_by(url: value) || ''
end
SiteSetting.set_and_log(id, value, current_user)

View File

@ -98,6 +98,8 @@ class TagsController < ::ApplicationController
end
def show
raise Discourse::NotFound if DiscourseTagging.hidden_tag_names(guardian).include?(params[:tag_id])
show_latest
end

View File

@ -219,7 +219,7 @@ module ApplicationHelper
opts[:twitter_summary_large_image] = twitter_summary_large_image_url
end
opts[:image] = SiteSetting.opengraph_image_url.presence ||
opts[:image] = SiteSetting.site_opengraph_image_url.presence ||
twitter_summary_large_image_url.presence ||
SiteSetting.site_large_icon_url.presence ||
SiteSetting.site_apple_touch_icon_url.presence ||
@ -295,7 +295,7 @@ module ApplicationHelper
if mobile_view? && SiteSetting.site_mobile_logo_url
SiteSetting.site_mobile_logo_url
else
SiteSetting.site_home_logo_url
SiteSetting.site_logo_url
end
end
end

View File

@ -22,8 +22,7 @@ module UserNotificationsHelper
logo_url = SiteSetting.site_digest_logo_url
logo_url = SiteSetting.site_logo_url if logo_url.blank? || logo_url =~ /\.svg$/i
return nil if logo_url.blank? || logo_url =~ /\.svg$/i
full_cdn_url(logo_url)
logo_url
end
def html_site_link(color)

View File

@ -0,0 +1,15 @@
require_dependency "post_alerter"
module Jobs
class NotifyTagChange < Jobs::Base
def execute(args)
post = Post.find_by(id: args[:post_id])
if post&.topic
post_alerter = PostAlerter.new
post_alerter.notify_post_users(post, User.where(id: args[:notified_user_ids]))
post_alerter.notify_first_post_watchers(post, post_alerter.tag_watchers(post.topic))
end
end
end
end

View File

@ -54,7 +54,10 @@ module Jobs
)
if message
Email::Sender.new(message, type, user).send
Email::Sender.new(message, type, user).send(
is_critical: self.class == Jobs::CriticalUserEmail
)
if (b = user.user_stat.bounce_score) > SiteSetting.bounce_score_erode_on_send
# erode bounce score each time we send an email
# this means that we are punished a lot less for bounces

View File

@ -7,6 +7,7 @@ module Jobs
# always remove invalid upload records
Upload
.by_users
.where("retain_hours IS NULL OR created_at < current_timestamp - interval '1 hour' * retain_hours")
.where("created_at < ?", grace_period.hour.ago)
.where(url: "")
@ -44,7 +45,8 @@ module Jobs
end
end.compact.uniq
result = Upload.where("uploads.retain_hours IS NULL OR uploads.created_at < current_timestamp - interval '1 hour' * uploads.retain_hours")
result = Upload.by_users
.where("uploads.retain_hours IS NULL OR uploads.created_at < current_timestamp - interval '1 hour' * uploads.retain_hours")
.where("uploads.created_at < ?", grace_period.hour.ago)
.joins(<<~SQL)
LEFT JOIN site_settings ss

View File

@ -20,7 +20,7 @@ module Jobs
if count > 0
target_usernames = Group[:moderators].users.map do |user|
next if user.id < 0
next if user.bot?
unseen_count = user.notifications.joins(:topic)
.where("notifications.id > ?", user.seen_notification_id)

View File

@ -133,7 +133,7 @@ InviteRedeemer = Struct.new(:invite, :username, :name, :password, :user_custom_f
def approve_account_if_needed
if get_existing_user
invited_user.approve(invite.invited_by_id, false)
invited_user.approve(invite.invited_by, false)
end
end

View File

@ -215,8 +215,7 @@ class Post < ActiveRecord::Base
end
def matches_recent_post?
post_id = $redis.get(unique_post_key)
post_id != (nil) && post_id.to_i != (id)
$redis.get(unique_post_key)&.to_i != id
end
def raw_hash

View File

@ -1529,7 +1529,9 @@ class Report
FROM uploads up
JOIN users u
ON u.id = up.user_id
WHERE up.created_at >= '#{report.start_date}' AND up.created_at <= '#{report.end_date}'
WHERE up.created_at >= '#{report.start_date}'
AND up.created_at <= '#{report.end_date}'
AND up.id > #{Upload::SEEDED_ID_THRESHOLD}
ORDER BY up.filesize DESC
LIMIT #{report.limit || 250}
SQL
@ -1548,6 +1550,54 @@ class Report
end
end
def self.report_top_ignored_users(report)
report.modes = [:table]
report.labels = [
{
type: :user,
properties: {
id: :ignored_user_id,
username: :ignored_username,
avatar: :ignored_user_avatar_template,
},
title: I18n.t("reports.top_ignored_users.labels.ignored_user")
},
{
type: :number,
properties: [
:ignores_count,
],
title: I18n.t("reports.top_ignored_users.labels.ignores_count")
}
]
report.data = []
sql = <<~SQL
SELECT
u.id AS user_id,
u.username,
u.uploaded_avatar_id,
COUNT(*) AS ignores_count
FROM users AS u
INNER JOIN ignored_users AS ig ON ig.ignored_user_id = u.id
WHERE ig.created_at >= '#{report.start_date}' AND ig.created_at <= '#{report.end_date}'
GROUP BY u.id
ORDER BY COUNT(*) DESC
LIMIT #{report.limit || 250}
SQL
DB.query(sql).each do |row|
report.data << {
ignored_user_id: row.user_id,
ignored_username: row.username,
ignored_user_avatar_template: User.avatar_template(row.username, row.uploaded_avatar_id),
ignores_count: row.ignores_count,
}
end
end
DiscourseEvent.on(:site_setting_saved) do |site_setting|
if ["backup_location", "s3_backup_bucket"].include?(site_setting.name.to_s)
clear_cache(:storage_stats)

View File

@ -175,69 +175,26 @@ class SiteSetting < ActiveRecord::Base
site_logo_small_url
site_mobile_logo_url
site_favicon_url
site_home_logo_url
}.each { |client_setting| client_settings << client_setting }
def self.site_home_logo_url
upload = SiteSetting.logo
if SiteSetting.defaults.get(:title) != SiteSetting.title && !upload
''
else
full_cdn_url(upload ? upload.url : '/images/d-logo-sketch.png')
%i{
logo
logo_small
digest_logo
mobile_logo
large_icon
favicon
apple_touch_icon
twitter_summary_large_image
opengraph_image
push_notifications_icon
}.each do |setting_name|
define_singleton_method("site_#{setting_name}_url") do
upload = self.public_send(setting_name)
upload ? full_cdn_url(upload.url) : ''
end
end
def self.site_logo_url
upload = self.logo
upload ? full_cdn_url(upload.url) : self.logo_url(warn: false)
end
def self.site_logo_small_url
upload = self.logo_small
upload ? full_cdn_url(upload.url) : self.logo_small_url(warn: false)
end
def self.site_digest_logo_url
upload = self.digest_logo
upload ? full_cdn_url(upload.url) : self.digest_logo_url(warn: false)
end
def self.site_mobile_logo_url
upload = self.mobile_logo
upload ? full_cdn_url(upload.url) : self.mobile_logo_url(warn: false)
end
def self.site_large_icon_url
upload = self.large_icon
upload ? full_cdn_url(upload.url) : self.large_icon_url(warn: false)
end
def self.site_favicon_url
upload = self.favicon
upload ? full_cdn_url(upload.url) : self.favicon_url(warn: false)
end
def self.site_apple_touch_icon_url
upload = self.apple_touch_icon
upload ? full_cdn_url(upload.url) : self.apple_touch_icon_url(warn: false)
end
def self.opengraph_image_url
upload = self.opengraph_image
upload ? full_cdn_url(upload.url) : self.default_opengraph_image_url(warn: false)
end
def self.site_twitter_summary_large_image_url
self.twitter_summary_large_image&.url ||
self.twitter_summary_large_image_url(warn: false)
end
def self.site_push_notifications_icon_url
SiteSetting.push_notifications_icon&.url ||
SiteSetting.push_notifications_icon_url(warn: false)
end
def self.shared_drafts_enabled?
c = SiteSetting.shared_drafts_category
c.present? && c.to_i != SiteSetting.uncategorized_category_id.to_i

View File

@ -32,7 +32,8 @@ class SkippedEmailLog < ActiveRecord::Base
sender_message_to_blank: 17,
sender_text_part_body_blank: 18,
sender_body_blank: 19,
sender_post_deleted: 20
sender_post_deleted: 20,
sender_message_to_invalid: 21
# you need to add the reason in server.en.yml below the "skipped_email_log" key
# when you add a new enum value
)

View File

@ -12,6 +12,8 @@ class TagGroup < ActiveRecord::Base
before_create :init_permissions
before_save :apply_permissions
after_commit { DiscourseTagging.clear_cache! }
attr_accessor :permissions
def tag_names=(tag_names_arg)

View File

@ -10,6 +10,7 @@ class Upload < ActiveRecord::Base
include ActionView::Helpers::NumberHelper
SHA1_LENGTH = 40
SEEDED_ID_THRESHOLD = 0
belongs_to :user
@ -36,6 +37,8 @@ class Upload < ActiveRecord::Base
UserAvatar.where(custom_upload_id: self.id).update_all(custom_upload_id: nil)
end
scope :by_users, -> { where("uploads.id > ?", SEEDED_ID_THRESHOLD) }
def to_s
self.url
end

View File

@ -294,6 +294,10 @@ class User < ActiveRecord::Base
self.id > 0
end
def bot?
!self.human?
end
def effective_locale
if SiteSetting.allow_user_locale && self.locale.present?
self.locale
@ -401,22 +405,18 @@ class User < ActiveRecord::Base
end
# Approve this user
def approve(approved_by, send_mail = true)
def approve(approver, send_mail = true)
self.approved = true
if approved_by.is_a?(Integer)
self.approved_by_id = approved_by
else
self.approved_by = approved_by
end
self.approved_at = Time.zone.now
self.approved_by = approver
if result = save
send_approval_email if send_mail
DiscourseEvent.trigger(:user_approved, self)
end
StaffActionLogger.new(approver).log_user_approve(self)
result
end

View File

@ -2,7 +2,8 @@ class UserField < ActiveRecord::Base
include AnonCacheInvalidator
validates_presence_of :name, :description, :field_type
validates_presence_of :description, :field_type
validates_presence_of :name, unless: -> { field_type == "confirm" }
has_many :user_field_options, dependent: :destroy
accepts_nested_attributes_for :user_field_options

View File

@ -84,7 +84,8 @@ class UserHistory < ActiveRecord::Base
merge_user: 65,
entity_export: 66,
change_password: 67,
topic_timestamps_changed: 68
topic_timestamps_changed: 68,
approve_user: 69
)
end
@ -147,7 +148,8 @@ class UserHistory < ActiveRecord::Base
:merge_user,
:entity_export,
:change_name,
:topic_timestamps_changed
:topic_timestamps_changed,
:approve_user
]
end

View File

@ -8,8 +8,6 @@ class UserProfile < ActiveRecord::Base
validates :user, presence: true
before_save :cook
after_save :trigger_badges
after_commit :trigger_profile_created_event, on: :create
after_commit :trigger_profile_updated_event, on: :update
validates :profile_background, upload_url: true, if: :profile_background_changed?
validates :card_background, upload_url: true, if: :card_background_changed?
@ -108,14 +106,6 @@ class UserProfile < ActiveRecord::Base
tempfile.close! if tempfile && tempfile.respond_to?(:close!)
end
def trigger_profile_created_event
DiscourseEvent.trigger(:user_profile_created, self)
end
def trigger_profile_updated_event
DiscourseEvent.trigger(:user_profile_updated, self)
end
protected
def trigger_badges

View File

@ -370,7 +370,9 @@ class PostSerializer < BasicPostSerializer
end
def include_post_notice_type?
return false if scope.user&.id == object.user_id || !scope.user&.has_trust_level?(TrustLevel[2])
return false if !scope.user || !scope.user.id || scope.user.id == object.user_id ||
!object.user || object.user.anonymous? ||
!scope.user.has_trust_level?(SiteSetting.min_post_notice_tl)
post_notice_type.present?
end

View File

@ -1,3 +1,7 @@
class TagSerializer < ApplicationSerializer
attributes :id, :name, :topic_count
attributes :id, :name, :topic_count, :staff
def staff
DiscourseTagging.staff_tag_names.include?(name)
end
end

View File

@ -13,7 +13,7 @@ class PostAlerter
def not_allowed?(user, post)
user.blank? ||
user.id < 0 ||
user.bot? ||
user.id == post.user_id
end
@ -279,8 +279,7 @@ class PostAlerter
DiscourseEvent.trigger(:before_create_notification, user, type, post, opts)
return if user.blank?
return if user.id < 0
return if user.blank? || user.bot?
is_liked = type == Notification.types[:liked]
return if is_liked && user.user_option.like_notification_frequency == UserOption.like_notification_frequency_type[:never]

View File

@ -67,8 +67,8 @@ class PushNotificationPusher
protected
def self.get_badge
if SiteSetting.site_push_notifications_icon_url.present?
SiteSetting.site_push_notifications_icon_url
if (url = SiteSetting.site_push_notifications_icon_url).present?
url
else
ActionController::Base.helpers.image_url("push-notifications/discourse.png")
end

View File

@ -503,6 +503,13 @@ class StaffActionLogger
))
end
def log_user_approve(user, opts = {})
UserHistory.create!(params(opts).merge(
action: UserHistory.actions[:approve_user],
target_user_id: user.id
))
end
def log_user_deactivate(user, reason, opts = {})
raise Discourse::InvalidParameters.new(:user) unless user
UserHistory.create!(params(opts).merge(

View File

@ -1927,21 +1927,36 @@ en:
actions:
recover: "Un-Delete Topic"
recover_tooltip: "Restores the topic."
delete: "Delete Topic"
delete_tooltip: "Marks the topic as deleted."
open: "Open Topic"
open_tooltip: "Marks the topic as open and allows new replies."
close: "Close Topic"
close_tooltip: "Marks the topic as closed and prevents new replies."
multi_select: "Select Posts…"
multi_select_tooltip: "Toggles multiselect mode in which multiple posts can be selected."
timed_update: "Set Topic Timer..."
timed_update_tooltip: "Opens a new window in which a timer for different actions can be set."
pin: "Pin Topic…"
pin_tooltip: "Opens a window in which the topic can be featured in different ways."
unpin: "Un-Pin Topic…"
unpin_tooltip: "Unpins the topic and it will no longer be featured."
unarchive: "Unarchive Topic"
unarchive_tooltip: "Unfreezes the topic and allows interacting with it."
archive: "Archive Topic"
archive_tooltip: "Freezes the topic and prevents interacting with it."
invisible: "Make Unlisted"
invisible_tooltip: "Prevents the inclusion of the topic in lists and digest emails."
visible: "Make Listed"
visible_tooltip: "Marks the topic as visible and includes the topic in lists and digest emails."
reset_read: "Reset Read Data"
make_public: "Make Public Topic"
make_public_tooltip: "Converts to public topic and makes it visible for other users."
make_private: "Make Personal Message"
make_private_tooltip: "Converts to personal message and makes it invisible for other users."
reset_bump_date: "Reset Bump Date"
reset_bump_date_tooltip: "Puts the topic back into chronological order according to when it was originally created."
feature:
pin: "Pin Topic"
@ -2099,6 +2114,7 @@ en:
change_timestamp:
title: "Change Timestamp..."
tooltip: "Opens a window in which the timestamp can be changed."
action: "change timestamp"
invalid_timestamp: "Timestamp cannot be in the future."
error: "There was an error changing the timestamp of the topic."
@ -2974,6 +2990,7 @@ en:
old_posts: "Old Flagged Posts"
topics: "Flagged Topics"
moderation_history: "Moderation History"
moderation_history_tooltip: "Shows the moderation history."
agree: "Agree"
agree_title: "Confirm this flag as valid and correct"
@ -3689,6 +3706,7 @@ en:
entity_export: "export entity"
change_name: "change name"
topic_timestamps_changed: "topic timestamps changed"
approve_user: "approved user"
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."

View File

@ -1227,6 +1227,12 @@ en:
author: Author
filesize: File size
description: "List all uploads by extension, filesize and author."
top_ignored_users:
title: "Top Ignored Users"
labels:
ignored_user: Ignored User
ignores_count: Ignores count
description: "List top ignored users."
dashboard:
rails_env_warning: "Your server is running in %{env} mode."
@ -1400,6 +1406,7 @@ en:
post_menu_hidden_items: "The menu items to hide by default in the post menu unless an expansion ellipsis is clicked on."
share_links: "Determine which items appear on the share dialog, and in what order."
site_contact_username: "A valid staff username to send all automated messages from. If left blank the default System account will be used."
site_contact_group_name: "A valid group name to be invited to all automated messages."
send_welcome_message: "Send all new users a welcome message with a quick start guide."
send_tl1_welcome_message: "Send new trust level 1 users a welcome message."
suppress_reply_directly_below: "Don't show the expandable reply count on a post when there is only a single reply directly below this post."
@ -1870,6 +1877,7 @@ en:
embed_truncate: "Truncate the embedded posts."
embed_support_markdown: "Support Markdown formatting for embedded posts."
embed_whitelist_selector: "A comma separated list of CSS elements that are allowed in embeds."
allowed_href_schemes: "Schemes allowed in links in addition to http and https."
embed_post_limit: "Maximum number of posts to embed."
embed_username_required: "The username for topic creation is required."
@ -1903,6 +1911,7 @@ en:
max_allowed_message_recipients: "Maximum recipients allowed in a message."
watched_words_regular_expressions: "Watched words are regular expressions."
min_post_notice_tl: "Minimum trust level required to see post notices."
returning_users_days: "How many days should pass before a user is considered to be returning."
default_email_digest_frequency: "How often users receive summary emails by default."
@ -1979,6 +1988,7 @@ en:
errors:
invalid_email: "Invalid email address."
invalid_username: "There's no user with that username."
invalid_group: "There's no group with that name."
invalid_integer_min_max: "Value must be between %{min} and %{max}."
invalid_integer_min: "Value must be %{min} or greater."
invalid_integer_max: "Value cannot be higher than %{max}."
@ -3455,6 +3465,7 @@ en:
sender_text_part_body_blank: "text_part.body is blank"
sender_body_blank: "body is blank"
sender_post_deleted: "post has been deleted"
sender_message_to_invalid: "recipient has invalid email address"
color_schemes:
base_theme_name: "Base"

View File

@ -1,6 +1,6 @@
# Available options:
#
# default - The default value of the setting.
# default - The default value of the setting. For upload site settings, use the id of the upload seeded in db/fixtures/010_uploads.rb.
# client - Set to true if the javascript should have access to this setting's value.
# refresh - Set to true if clients should refresh when the setting is changed.
# min - For a string setting, the minimum length. For an integer setting, the minimum value.
@ -47,15 +47,18 @@ required:
site_contact_username:
default: ""
type: username
logo:
site_contact_group_name:
default: ""
type: group
logo:
default: -1
client: true
type: upload
logo_url:
hidden: true
default: "/images/d-logo-sketch.png"
logo_small:
default: ""
default: -2
client: true
type: upload
logo_small_url:
@ -83,14 +86,14 @@ required:
hidden: true
default: ""
favicon:
default: ""
default: -3
client: true
type: upload
favicon_url:
hidden: true
default: "/images/default-favicon.ico"
apple_touch_icon:
default: ""
default: -4
client: true
type: upload
apple_touch_icon_url:
@ -691,7 +694,13 @@ posting:
max_users_notified_per_group_mention: 100
newuser_max_replies_per_topic: 3
newuser_max_mentions_per_post: 2
title_max_word_length: 30
title_max_word_length:
default: 30
locale_default:
ja: 50
ko: 50
zh_CN: 50
zh_TW: 50
whitelisted_link_domains:
default: ""
type: list
@ -790,6 +799,8 @@ posting:
default: true
embed_support_markdown:
default: false
embed_whitelist_selector:
default: ""
allowed_href_schemes:
client: true
default: ""
@ -806,6 +817,9 @@ posting:
default: false
client: true
shadowed_by_global: true
min_post_notice_tl:
default: 2
enum: "TrustLevelSetting"
returning_users_days:
default: 60
@ -1473,9 +1487,6 @@ embedding:
embed_title_scrubber:
default: ""
hidden: true
embed_whitelist_selector:
default: ""
hidden: true
embed_blacklist_selector:
default: ""
hidden: true

View File

@ -0,0 +1,17 @@
{
-1 => "d-logo-sketch.png",
-2 => "d-logo-sketch-small.png",
-3 => "default-favicon.ico",
-4 => "default-apple-touch-icon.png"
}.each do |id, filename|
path = Rails.root.join("public/images/#{filename}")
Upload.seed do |upload|
upload.id = id
upload.user_id = Discourse.system_user.id
upload.original_filename = filename
upload.url = "/images/#{filename}"
upload.filesize = File.size(path)
upload.extension = File.extname(path)[1..10]
end
end

View File

@ -45,7 +45,7 @@ class Auth::DefaultCurrentUserProvider
request = @request
user_api_key = @env[USER_API_KEY]
api_key = @env.blank? ? nil : request[API_KEY] || @env[HEADER_API_KEY]
api_key = @env.blank? ? nil : @env[HEADER_API_KEY] || request[API_KEY]
auth_token = request.cookies[TOKEN_COOKIE] unless user_api_key || api_key
@ -284,7 +284,7 @@ class Auth::DefaultCurrentUserProvider
def lookup_api_user(api_key_value, request)
if api_key = ApiKey.where(key: api_key_value).includes(:user).first
api_username = request[API_USERNAME] || @env[HEADER_API_USERNAME]
api_username = header_api_key? ? @env[HEADER_API_USERNAME] : request[API_USERNAME]
if api_key.allowed_ips.present? && !api_key.allowed_ips.any? { |ip| ip.include?(request.ip) }
Rails.logger.warn("[Unauthorized API Access] username: #{api_username}, IP address: #{request.ip}")
@ -295,9 +295,9 @@ class Auth::DefaultCurrentUserProvider
api_key.user if !api_username || (api_key.user.username_lower == api_username.downcase)
elsif api_username
User.find_by(username_lower: api_username.downcase)
elsif user_id = request["api_user_id"] || @env[HEADER_API_USER_ID]
elsif user_id = header_api_key? ? @env[HEADER_API_USER_ID] : request["api_user_id"]
User.find_by(id: user_id.to_i)
elsif external_id = request["api_user_external_id"] || @env[HEADER_API_USER_EXTERNAL_ID]
elsif external_id = header_api_key? ? @env[HEADER_API_USER_EXTERNAL_ID] : request["api_user_external_id"]
SingleSignOnRecord.find_by(external_id: external_id.to_s).try(:user)
end
end
@ -305,6 +305,10 @@ class Auth::DefaultCurrentUserProvider
private
def header_api_key?
!!@env[HEADER_API_KEY]
end
def rate_limit_admin_api_requests(api_key)
return if Rails.env == "profile"

View File

@ -2,6 +2,7 @@ module DiscourseTagging
TAGS_FIELD_NAME = "tags"
TAGS_FILTER_REGEXP = /[\/\?#\[\]@!\$&'\(\)\*\+,;=\.%\\`^\s|\{\}"<>]+/ # /?#[]@!$&'()*+,;=.%\`^|{}"<>
TAGS_STAFF_CACHE_KEY = "staff_tag_names"
def self.tag_topic_by_names(topic, guardian, tag_names_arg, append: false)
if guardian.can_tag?(topic)
@ -194,11 +195,22 @@ module DiscourseTagging
end
def self.staff_tag_names
Tag.joins(tag_groups: :tag_group_permissions)
.where('tag_group_permissions.group_id = ? AND tag_group_permissions.permission_type = ?',
Group::AUTO_GROUPS[:everyone],
TagGroupPermission.permission_types[:readonly]
).pluck(:name)
tag_names = Discourse.cache.read(TAGS_STAFF_CACHE_KEY, tag_names)
if !tag_names
tag_names = Tag.joins(tag_groups: :tag_group_permissions)
.where('tag_group_permissions.group_id = ? AND tag_group_permissions.permission_type = ?',
Group::AUTO_GROUPS[:everyone],
TagGroupPermission.permission_types[:readonly]
).pluck(:name)
Discourse.cache.write(TAGS_STAFF_CACHE_KEY, tag_names, expires_in: 1.hour)
end
tag_names
end
def self.clear_cache!
Discourse.cache.delete(TAGS_STAFF_CACHE_KEY)
end
def self.clean_tag(tag)

View File

@ -21,8 +21,13 @@ module Email
@user = user
end
def send
return if SiteSetting.disable_emails == "yes" && @email_type.to_s != "admin_login"
def send(is_critical: false)
if SiteSetting.disable_emails == "yes" &&
@email_type.to_s != "admin_login" &&
!is_critical
return
end
return if ActionMailer::Base::NullMail === @message
return if ActionMailer::Base::NullMail === (@message.message rescue nil)
@ -30,6 +35,8 @@ module Email
return skip(SkippedEmailLog.reason_types[:sender_message_blank]) if @message.blank?
return skip(SkippedEmailLog.reason_types[:sender_message_to_blank]) if @message.to.blank?
return skip(SkippedEmailLog.reason_types[:sender_message_to_invalid]) if to_address.end_with?(".invalid")
if @message.text_part
if @message.text_part.body.to_s.blank?
return skip(SkippedEmailLog.reason_types[:sender_text_part_body_blank])

View File

@ -92,10 +92,6 @@ module FileStore
def purge_tombstone(grace_period)
end
def get_depth_for(id)
[0, Math.log(id / 1_000.0, 16).ceil].max
end
def get_path_for(type, id, sha, extension)
depth = get_depth_for(id)
tree = File.join(*sha[0, depth].chars, "")
@ -148,6 +144,12 @@ module FileStore
raise "Not implemented."
end
def get_depth_for(id)
depths = [0]
depths << Math.log(id / 1_000.0, 16).ceil if id.positive?
depths.max
end
end
end

View File

@ -129,7 +129,7 @@ module FileStore
S3Inventory.new(s3_helper, :upload).backfill_etags_and_list_missing
S3Inventory.new(s3_helper, :optimized).backfill_etags_and_list_missing unless skip_optimized
else
list_missing(Upload, "original/")
list_missing(Upload.by_users, "original/")
list_missing(OptimizedImage, "optimized/") unless skip_optimized
end
end

View File

@ -2,7 +2,7 @@ require 'rails_helper'
describe <%= name %>::ActionsController do
before do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
end
it 'can list' do

View File

@ -514,7 +514,7 @@ class PostCreator
end
def create_post_notice
return if @user.id < 0 || @user.staged
return if @user.bot? || @user.staged
last_post_time = Post.where(user_id: @user.id)
.order(created_at: :desc)

View File

@ -83,7 +83,14 @@ class PostRevisor
tc.check_result(false)
next
end
tc.record_change('tags', prev_tags, tags) unless prev_tags.sort == tags.sort
if prev_tags.sort != tags.sort
tc.record_change('tags', prev_tags, tags)
DB.after_commit do
post = tc.topic.ordered_posts.first
notified_user_ids = [post.user_id, post.last_editor_id].uniq
Jobs.enqueue(:notify_tag_change, post_id: post.id, notified_user_ids: notified_user_ids)
end
end
end
end

View File

@ -54,7 +54,7 @@ class S3Inventory
WHERE #{model.table_name}.etag IS NULL
AND url ILIKE '%' || #{table_name}.key")
uploads = (model == Upload) ? model.where("created_at < ?", inventory_date) : model
uploads = (model == Upload) ? model.by_users.where("created_at < ?", inventory_date) : model
missing_uploads = uploads.joins("LEFT JOIN #{table_name} ON #{table_name}.etag = #{model.table_name}.etag").where("#{table_name}.etag is NULL")
if (missing_count = missing_uploads.count) > 0

View File

@ -394,22 +394,33 @@ class Search
exact = true
slug = match.to_s.split(":")
next if slug.empty?
category_slug, subcategory_slug = match.to_s.split(":")
next unless category_slug
if slug[1]
if subcategory_slug
# sub category
parent_category_id = Category.where(slug: slug[0].downcase, parent_category_id: nil).pluck(:id).first
category_id = Category.where(slug: slug[1].downcase, parent_category_id: parent_category_id).pluck(:id).first
parent_category_id = Category
.where(
"lower(slug) = ? AND parent_category_id IS NULL", category_slug.downcase
)
.pluck(:id)
.first
category_id = Category
.where("lower(slug) = ? AND parent_category_id = ?",
subcategory_slug.downcase, parent_category_id
)
.pluck(:id)
.first
else
# main category
if slug[0][0] == "="
slug[0] = slug[0][1..-1]
if category_slug[0] == "="
category_slug = category_slug[1..-1]
else
exact = false
end
category_id = Category.where(slug: slug[0].downcase)
category_id = Category.where("lower(slug) = ?", category_slug.downcase)
.order('case when parent_category_id is null then 0 else 1 end')
.pluck(:id)
.first
@ -425,7 +436,7 @@ class Search
posts.where("topics.category_id IN (?)", category_ids)
else
# try a possible tag match
tag_id = Tag.where_name(slug[0]).pluck(:id).first
tag_id = Tag.where_name(category_slug).pluck(:id).first
if (tag_id)
posts.where("topics.id IN (
SELECT DISTINCT(tt.topic_id)

View File

@ -230,17 +230,25 @@ module SiteSettingExtension
.map do |s, v|
value = send(s)
type_hash = type_supervisor.type_hash(s)
default = defaults.get(s, default_locale).to_s
if type_hash[:type].to_s == "upload" &&
default.to_i < Upload::SEEDED_ID_THRESHOLD
default = default_uploads[default.to_i]
end
opts = {
setting: s,
description: description(s),
default: defaults.get(s, default_locale).to_s,
default: default,
value: value.to_s,
category: categories[s],
preview: previews[s],
secret: secret_settings.include?(s),
placeholder: placeholder(s)
}.merge(type_supervisor.type_hash(s))
}.merge!(type_hash)
opts
end.unshift(locale_setting_hash)
@ -450,7 +458,7 @@ module SiteSettingExtension
value = value.to_i
if value > 0
if value != Upload::SEEDED_ID_THRESHOLD
upload = Upload.find_by(id: value)
uploads[name] = upload if upload
end
@ -495,6 +503,14 @@ module SiteSettingExtension
private
def default_uploads
@default_uploads ||= {}
@default_uploads[provider.current_site] ||= begin
Upload.where("id < ?", Upload::SEEDED_ID_THRESHOLD).pluck(:id, :url).to_h
end
end
def uploads
@uploads ||= {}
@uploads[provider.current_site] ||= {}

View File

@ -32,6 +32,7 @@ class SiteSettings::TypeSupervisor
category: 16,
uploaded_image_list: 17,
upload: 18,
group: 19,
)
end
@ -177,7 +178,7 @@ class SiteSettings::TypeSupervisor
elsif type == self.class.types[:enum]
val = @defaults_provider[name].is_a?(Integer) ? val.to_i : val.to_s
elsif type == self.class.types[:upload] && val.present?
val = val.id
val = val.is_a?(Integer) ? val : val.id
end
[val, type]
@ -239,6 +240,8 @@ class SiteSettings::TypeSupervisor
EmailSettingValidator
when self.class.types[:username]
UsernameSettingValidator
when self.class.types[:group]
GroupSettingValidator
when self.class.types[:integer]
IntegerSettingValidator
when self.class.types[:regex]

View File

@ -28,6 +28,7 @@ class SystemMessage
raw: raw,
archetype: Archetype.private_message,
target_usernames: @recipient.username,
target_group_names: Group.exists?(name: SiteSetting.site_contact_group_name) ? SiteSetting.site_contact_group_name : nil,
subtype: TopicSubtype.system_message,
skip_validations: true)

View File

@ -118,6 +118,8 @@ task 'docker:test' do
puts "travis_fold:start:ruby_tests" if ENV["TRAVIS"]
unless ENV["SKIP_CORE"]
params = []
params << "--profile"
params << "--fail-fast"
if ENV["BISECT"]
params << "--bisect"
end

View File

@ -7,7 +7,8 @@ task 'plugin:install_all_official' do
'discourse-nginx-performance-report',
'lazyYT',
'poll',
'discourse-calendar'
'discourse-calendar',
'discourse-chat-integration'
])
map = {

View File

@ -0,0 +1,14 @@
class GroupSettingValidator
def initialize(opts = {})
@opts = opts
end
def valid_value?(val)
val.blank? || Group.exists?(name: val)
end
def error_message
I18n.t('site_settings.errors.invalid_group')
end
end

View File

@ -7,7 +7,7 @@ module Discourse
MAJOR = 2
MINOR = 3
TINY = 0
PRE = 'beta4'
PRE = 'beta5'
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
end

View File

@ -3,7 +3,7 @@ require 'rails_helper'
describe Post do
before do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
end
describe '#local_dates' do

View File

@ -22,7 +22,7 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do
let(:reset_trigger) { DiscourseNarrativeBot::TrackSelector.reset_trigger }
before do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
SiteSetting.discourse_narrative_bot_enabled = true
end

View File

@ -25,7 +25,7 @@ describe DiscourseNarrativeBot::NewUserNarrative do
let(:reset_trigger) { DiscourseNarrativeBot::TrackSelector.reset_trigger }
before do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
SiteSetting.discourse_narrative_bot_enabled = true
end

View File

@ -37,7 +37,7 @@ describe DiscourseNarrativeBot::TrackSelector do
end
before do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
end
describe '#select' do

View File

@ -27,7 +27,7 @@ describe "Discobot Certificate" do
it 'should return the right text' do
stub_request(:get, /letter_avatar_proxy/).to_return(status: 200)
stub_request(:get, "http://test.localhost//images/d-logo-sketch-small.png")
stub_request(:get, SiteSetting.site_logo_small_url)
.to_return(status: 200)
get '/discobot/certificate.svg', params: params

View File

@ -13,7 +13,7 @@ describe User do
end
before do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
SiteSetting.discourse_narrative_bot_enabled = true
end

View File

@ -92,6 +92,15 @@ describe Auth::DefaultCurrentUserProvider do
expect(provider("/?api_key=hello&api_username=#{user.username.downcase}").current_user.id).to eq(user.id)
end
it "raises for a mismatched api_key param and header username" do
user = Fabricate(:user)
ApiKey.create!(key: "hello", created_by_id: -1)
params = { "HTTP_API_USERNAME" => user.username.downcase }
expect {
provider("/?api_key=hello", params).current_user
}.to raise_error(Discourse::InvalidAccess)
end
it "finds a user for a correct system api key with external id" do
user = Fabricate(:user)
ApiKey.create!(key: "hello", created_by_id: -1)
@ -99,12 +108,31 @@ describe Auth::DefaultCurrentUserProvider do
expect(provider("/?api_key=hello&api_user_external_id=abc").current_user.id).to eq(user.id)
end
it "raises for a mismatched api_key param and header external id" do
user = Fabricate(:user)
ApiKey.create!(key: "hello", created_by_id: -1)
SingleSignOnRecord.create(user_id: user.id, external_id: "abc", last_payload: '')
params = { "HTTP_API_USER_EXTERNAL_ID" => "abc" }
expect {
provider("/?api_key=hello", params).current_user
}.to raise_error(Discourse::InvalidAccess)
end
it "finds a user for a correct system api key with id" do
user = Fabricate(:user)
ApiKey.create!(key: "hello", created_by_id: -1)
expect(provider("/?api_key=hello&api_user_id=#{user.id}").current_user.id).to eq(user.id)
end
it "raises for a mismatched api_key param and header user id" do
user = Fabricate(:user)
ApiKey.create!(key: "hello", created_by_id: -1)
params = { "HTTP_API_USER_ID" => user.id }
expect {
provider("/?api_key=hello", params).current_user
}.to raise_error(Discourse::InvalidAccess)
end
context "rate limiting" do
before do
RateLimiter.enable
@ -243,6 +271,15 @@ describe Auth::DefaultCurrentUserProvider do
expect(provider("/", params).current_user.id).to eq(user.id)
end
it "raises for a mismatched api_key header and param username" do
user = Fabricate(:user)
ApiKey.create!(key: "hello", created_by_id: -1)
params = { "HTTP_API_KEY" => "hello" }
expect {
provider("/?api_username=#{user.username.downcase}", params).current_user
}.to raise_error(Discourse::InvalidAccess)
end
it "finds a user for a correct system api key with external id" do
user = Fabricate(:user)
ApiKey.create!(key: "hello", created_by_id: -1)
@ -251,6 +288,16 @@ describe Auth::DefaultCurrentUserProvider do
expect(provider("/", params).current_user.id).to eq(user.id)
end
it "raises for a mismatched api_key header and param external id" do
user = Fabricate(:user)
ApiKey.create!(key: "hello", created_by_id: -1)
SingleSignOnRecord.create(user_id: user.id, external_id: "abc", last_payload: '')
params = { "HTTP_API_KEY" => "hello" }
expect {
provider("/?api_user_external_id=abc", params).current_user
}.to raise_error(Discourse::InvalidAccess)
end
it "finds a user for a correct system api key with id" do
user = Fabricate(:user)
ApiKey.create!(key: "hello", created_by_id: -1)
@ -258,6 +305,15 @@ describe Auth::DefaultCurrentUserProvider do
expect(provider("/", params).current_user.id).to eq(user.id)
end
it "raises for a mismatched api_key header and param user id" do
user = Fabricate(:user)
ApiKey.create!(key: "hello", created_by_id: -1)
params = { "HTTP_API_KEY" => "hello" }
expect {
provider("/?api_user_id=#{user.id}", params).current_user
}.to raise_error(Discourse::InvalidAccess)
end
context "rate limiting" do
before do
RateLimiter.enable

View File

@ -216,4 +216,29 @@ describe DiscourseTagging do
end
end
end
describe "staff_tag_names" do
let(:tag) { Fabricate(:tag) }
let(:staff_tag) { Fabricate(:tag) }
let(:other_staff_tag) { Fabricate(:tag) }
let!(:staff_tag_group) {
Fabricate(
:tag_group,
permissions: { "staff" => 1, "everyone" => 3 },
tag_names: [staff_tag.name]
)
}
it "returns all staff tags" do
expect(DiscourseTagging.staff_tag_names).to contain_exactly(staff_tag.name)
staff_tag_group.update(tag_names: [staff_tag.name, other_staff_tag.name])
expect(DiscourseTagging.staff_tag_names).to contain_exactly(staff_tag.name, other_staff_tag.name)
staff_tag_group.update(tag_names: [other_staff_tag.name])
expect(DiscourseTagging.staff_tag_names).to contain_exactly(other_staff_tag.name)
end
end
end

View File

@ -58,6 +58,13 @@ describe Email::Sender do
Email::Sender.new(message, :hello).send
end
it "doesn't deliver when the to address uses the .invalid tld" do
message = Mail::Message.new(body: 'hello', to: 'myemail@example.invalid')
message.expects(:deliver_now).never
expect { Email::Sender.new(message, :hello).send }.
to change { SkippedEmailLog.where(reason_type: SkippedEmailLog.reason_types[:sender_message_to_invalid]).count }.by(1)
end
it "doesn't deliver when the body is nil" do
message = Mail::Message.new(to: 'eviltrout@test.domain')
message.expects(:deliver_now).never

View File

@ -18,6 +18,15 @@ RSpec.describe FileStore::BaseStore do
.to eq('original/2X/4/4170ac2a2782a1516fe9e13d7322ae482c1bd594.png')
end
end
describe 'when id is negative' do
it 'should return the right depth' do
upload.update!(id: -999)
expect(FileStore::BaseStore.new.get_path_for_upload(upload))
.to eq('original/1X/4170ac2a2782a1516fe9e13d7322ae482c1bd594.png')
end
end
end
describe '#get_path_for_optimized_image' do

View File

@ -902,7 +902,7 @@ describe PostCreator do
end
it 'can post to a group correctly' do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
expect(post.topic.archetype).to eq(Archetype.private_message)
expect(post.topic.topic_allowed_users.count).to eq(1)

View File

@ -617,7 +617,7 @@ describe PostDestroyer do
context '@mentions' do
it 'removes notifications when deleted' do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
user = Fabricate(:evil_trout)
post = create_post(raw: 'Hello @eviltrout')
expect {

View File

@ -608,7 +608,7 @@ describe PostRevisor do
let(:mentioned_user) { Fabricate(:user) }
before do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
end
it "generates a notification for a mention" do

View File

@ -61,6 +61,7 @@ describe "S3Inventory" do
CSV.foreach(csv_filename, headers: false) do |row|
Fabricate(:upload, etag: row[S3Inventory::CSV_ETAG_INDEX], created_at: 2.days.ago)
end
upload = Fabricate(:upload, etag: "ETag", created_at: 1.days.ago)
Fabricate(:upload, etag: "ETag2", created_at: Time.now)
@ -91,6 +92,6 @@ describe "S3Inventory" do
expect { inventory.backfill_etags_and_list_missing }.to change { Upload.where(etag: nil).count }.by(-2)
end
expect(Upload.order(:url).pluck(:url, :etag)).to eq(files)
expect(Upload.by_users.order(:url).pluck(:url, :etag)).to eq(files)
end
end

View File

@ -873,25 +873,28 @@ describe Search do
it 'supports category slug and tags' do
# main category
category = Fabricate(:category, name: 'category 24', slug: 'category-24')
category = Fabricate(:category, name: 'category 24', slug: 'cateGory-24')
topic = Fabricate(:topic, created_at: 3.months.ago, category: category)
post = Fabricate(:post, raw: 'Sams first post', topic: topic)
expect(Search.execute('sams post #category-24').posts.length).to eq(1)
expect(Search.execute('sams post #categoRy-24').posts.length).to eq(1)
expect(Search.execute("sams post category:#{category.id}").posts.length).to eq(1)
expect(Search.execute('sams post #category-25').posts.length).to eq(0)
expect(Search.execute('sams post #categoRy-25').posts.length).to eq(0)
sub_category = Fabricate(:category, name: 'sub category', slug: 'sub-category', parent_category_id: category.id)
second_topic = Fabricate(:topic, created_at: 3.months.ago, category: sub_category)
Fabricate(:post, raw: 'sams second post', topic: second_topic)
expect(Search.execute("sams post category:category-24").posts.length).to eq(2)
expect(Search.execute("sams post category:=category-24").posts.length).to eq(1)
expect(Search.execute("sams post category:categoRY-24").posts.length).to eq(2)
expect(Search.execute("sams post category:=cAtegory-24").posts.length).to eq(1)
expect(Search.execute("sams post #category-24").posts.length).to eq(2)
expect(Search.execute("sams post #=category-24").posts.length).to eq(1)
expect(Search.execute("sams post #sub-category").posts.length).to eq(1)
expect(Search.execute("sams post #categoRY-24:SUB-category").posts.length)
.to eq(1)
# tags
topic.tags = [Fabricate(:tag, name: 'alpha'), Fabricate(:tag, name: 'привет'), Fabricate(:tag, name: 'HeLlO')]
expect(Search.execute('this is a test #alpha').posts.map(&:id)).to eq([post.id])

View File

@ -711,6 +711,28 @@ describe SiteSettingExtension do
end
describe '.all_settings' do
describe 'uploads settings' do
it 'should return the right values' do
system_upload = Fabricate(:upload, id: -999)
settings.setting(:logo, system_upload.id, type: :upload)
settings.refresh!
setting = settings.all_settings.last
expect(setting[:value]).to eq(system_upload.url)
expect(setting[:default]).to eq(system_upload.url)
upload = Fabricate(:upload)
settings.logo = upload
settings.refresh!
setting = settings.all_settings.last
expect(setting[:value]).to eq(upload.url)
expect(setting[:default]).to eq(system_upload.url)
end
end
end
describe '.client_settings_json_uncached' do
it 'should return the right json value' do
upload = Fabricate(:upload)

View File

@ -190,6 +190,9 @@ describe SiteSettings::TypeSupervisor do
expect(settings.type_supervisor.to_db_value(:type_upload, upload))
.to eq([upload.id, SiteSetting.types[:upload]])
expect(settings.type_supervisor.to_db_value(:type_upload, 1))
.to eq([1, SiteSetting.types[:upload]])
end
it 'returns enum value with string default' do

View File

@ -6,17 +6,19 @@ describe SystemMessage do
context 'send' do
it 'should create a post correctly' do
let(:admin) { Fabricate(:admin) }
let(:user) { Fabricate(:user) }
admin = Fabricate(:admin)
user = Fabricate(:user)
before do
SiteSetting.site_contact_username = admin.username
end
it 'should create a post correctly' do
system_message = SystemMessage.new(user)
post = system_message.create(:welcome_invite)
topic = post.topic
expect(post).to be_present
expect(post).to be_valid
expect(post.valid?).to eq(true)
expect(topic).to be_private_message
expect(topic).to be_valid
expect(topic.subtype).to eq(TopicSubtype.system_message)
@ -25,6 +27,18 @@ describe SystemMessage do
expect(UserArchivedMessage.where(user_id: admin.id, topic_id: topic.id).length).to eq(1)
end
it 'should allow site_contact_group_name' do
group = Fabricate(:group)
SiteSetting.site_contact_group_name = group.name
post = SystemMessage.create(user, :welcome_invite)
expect(post.topic.allowed_groups).to contain_exactly(group)
group.update!(name: "anewname")
post = SystemMessage.create(user, :welcome_invite)
expect(post.topic.allowed_groups).to contain_exactly()
end
end
end

View File

@ -0,0 +1,21 @@
require 'rails_helper'
describe GroupSettingValidator do
describe '#valid_value?' do
subject(:validator) { described_class.new }
it "returns true for blank values" do
expect(validator.valid_value?('')).to eq(true)
expect(validator.valid_value?(nil)).to eq(true)
end
it "returns true if value matches an existing group" do
Fabricate(:group, name: "hello")
expect(validator.valid_value?('hello')).to eq(true)
end
it "returns false if value does not match a group" do
expect(validator.valid_value?('notagroup')).to eq(false)
end
end
end

View File

@ -274,7 +274,7 @@ describe ApplicationHelper do
).to include("some-image.png")
expect(helper.crawlable_meta_data).to include(
SiteSetting.opengraph_image_url
SiteSetting.site_opengraph_image_url
)
SiteSetting.opengraph_image = nil

View File

@ -163,7 +163,7 @@ describe WatchedWord do
end
it "flags on revisions" do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
post = Fabricate(:post, topic: Fabricate(:topic, user: tl2_user), user: tl2_user)
expect {
PostRevisor.new(post).revise!(post.user, { raw: "Want some #{flag_word.word} for cheap?" }, revised_at: post.updated_at + 10.seconds)

View File

@ -50,6 +50,7 @@ describe Jobs::CleanUpUploads do
SiteSetting.provider = SiteSettings::DbProvider.new(SiteSetting)
SiteSetting.clean_orphan_uploads_grace_period_hours = 1
system_upload = fabricate_upload(id: -999)
logo_upload = fabricate_upload
logo_small_upload = fabricate_upload
digest_logo_upload = fabricate_upload
@ -84,7 +85,8 @@ describe Jobs::CleanUpUploads do
opengraph_image_upload,
twitter_summary_large_image_upload,
favicon_upload,
apple_touch_icon_upload
apple_touch_icon_upload,
system_upload
].each { |record| expect(Upload.exists?(id: record.id)).to eq(true) }
fabricate_upload

View File

@ -32,7 +32,7 @@ describe Jobs::PullHotlinkedImages do
describe '#execute' do
before do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
FastImage.expects(:size).returns([100, 100]).at_least_once
end

View File

@ -11,7 +11,6 @@ describe Jobs::UserEmail do
let(:staged) { Fabricate(:user, staged: true, last_seen_at: 11.minutes.ago) }
let(:suspended) { Fabricate(:user, last_seen_at: 10.minutes.ago, suspended_at: 5.minutes.ago, suspended_till: 7.days.from_now) }
let(:anonymous) { Fabricate(:anonymous, last_seen_at: 11.minutes.ago) }
let(:mailer) { Mail::Message.new(to: user.email) }
it "raises an error when there is no user" do
expect { Jobs::UserEmail.new.execute(type: :digest) }.to raise_error(Discourse::InvalidParameters)
@ -26,13 +25,15 @@ describe Jobs::UserEmail do
end
it "doesn't call the mailer when the user is missing" do
UserNotifications.expects(:digest).never
Jobs::UserEmail.new.execute(type: :digest, user_id: 1234)
expect(ActionMailer::Base.deliveries).to eq([])
end
it "doesn't call the mailer when the user is staged" do
UserNotifications.expects(:digest).never
Jobs::UserEmail.new.execute(type: :digest, user_id: staged.id)
expect(ActionMailer::Base.deliveries).to eq([])
end
context "bounce score" do
@ -45,55 +46,48 @@ describe Jobs::UserEmail do
email_log = EmailLog.where(user_id: user.id).last
expect(email_log.email_type).to eq("signup")
expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(
user.email
)
end
end
context 'to_address' do
it 'overwrites a to_address when present' do
UserNotifications.expects(:confirm_new_email).returns(mailer)
Email::Sender.any_instance.expects(:send)
Jobs::UserEmail.new.execute(type: :confirm_new_email, user_id: user.id, to_address: 'jake@adventuretime.ooo')
expect(mailer.to).to eq(['jake@adventuretime.ooo'])
expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(
'jake@adventuretime.ooo'
)
end
end
context "disable_emails setting" do
it "sends when no" do
SiteSetting.disable_emails = 'no'
Email::Sender.any_instance.expects(:send).once
Jobs::UserEmail.new.execute(type: :confirm_new_email, user_id: user.id)
expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(
user.email
)
end
it "does not send an email when yes" do
SiteSetting.disable_emails = 'yes'
Email::Sender.any_instance.expects(:send).never
Jobs::UserEmail.new.execute(type: :confirm_new_email, user_id: user.id)
expect(ActionMailer::Base.deliveries).to eq([])
end
it "sends when critical" do
SiteSetting.disable_emails = 'yes'
Email::Sender.any_instance.expects(:send)
Jobs::CriticalUserEmail.new.execute(type: :confirm_new_email, user_id: user.id)
end
end
context "recently seen" do
let(:post) { Fabricate(:post, user: user) }
it "doesn't send an email to a user that's been recently seen" do
user.update_column(:last_seen_at, 9.minutes.ago)
Email::Sender.any_instance.expects(:send).never
Jobs::UserEmail.new.execute(type: :user_replied, user_id: user.id, post_id: post.id)
end
it "does send an email to a user that's been recently seen but has email_always set" do
user.update_attributes(last_seen_at: 9.minutes.ago)
user.user_option.update_attributes(email_always: true)
PostTiming.create!(topic_id: post.topic_id, post_number: post.post_number, user_id: user.id, msecs: 100)
Email::Sender.any_instance.expects(:send)
Jobs::UserEmail.new.execute(type: :user_replied, user_id: user.id, post_id: post.id)
expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(
user.email
)
end
end
@ -145,49 +139,66 @@ describe Jobs::UserEmail do
context 'args' do
it 'passes a token as an argument when a token is present' do
UserNotifications.expects(:forgot_password).with(user, email_token: 'asdfasdf').returns(mailer)
Email::Sender.any_instance.expects(:send)
Jobs::UserEmail.new.execute(type: :forgot_password, user_id: user.id, email_token: 'asdfasdf')
mail = ActionMailer::Base.deliveries.first
expect(mail.to).to contain_exactly(user.email)
expect(mail.body).to include("asdfasdf")
end
context "post" do
let(:post) { Fabricate(:post, user: user) }
it 'passes a post as an argument when a post_id is present' do
UserNotifications.expects(:user_private_message).with(user, post: post).returns(mailer)
Email::Sender.any_instance.expects(:send)
Jobs::UserEmail.new.execute(type: :user_private_message, user_id: user.id, post_id: post.id)
end
it "doesn't send the email if you've seen the post" do
Email::Sender.any_instance.expects(:send).never
PostTiming.record_timing(topic_id: post.topic_id, user_id: user.id, post_number: post.post_number, msecs: 6666)
Jobs::UserEmail.new.execute(type: :user_private_message, user_id: user.id, post_id: post.id)
expect(ActionMailer::Base.deliveries).to eq([])
end
it "doesn't send the email if the user deleted the post" do
Email::Sender.any_instance.expects(:send).never
post.update_column(:user_deleted, true)
Jobs::UserEmail.new.execute(type: :user_private_message, user_id: user.id, post_id: post.id)
expect(ActionMailer::Base.deliveries).to eq([])
end
it "doesn't send the email if user of the post has been deleted" do
Email::Sender.any_instance.expects(:send).never
post.update_attributes!(user_id: nil)
Jobs::UserEmail.new.execute(type: :user_replied, user_id: user.id, post_id: post.id)
expect(ActionMailer::Base.deliveries).to eq([])
end
context 'user is suspended' do
it "doesn't send email for a pm from a regular user" do
Email::Sender.any_instance.expects(:send).never
Jobs::UserEmail.new.execute(type: :user_private_message, user_id: suspended.id, post_id: post.id)
expect(ActionMailer::Base.deliveries).to eq([])
end
it "does send an email for a pm from a staff user" do
pm_from_staff = Fabricate(:post, user: Fabricate(:moderator))
pm_from_staff.topic.topic_allowed_users.create!(user_id: suspended.id)
Email::Sender.any_instance.expects(:send)
Jobs::UserEmail.new.execute(type: :user_private_message, user_id: suspended.id, post_id: pm_from_staff.id)
pm_notification = Fabricate(:notification,
user: suspended,
topic: pm_from_staff.topic,
post_number: pm_from_staff.post_number,
data: { original_post_id: pm_from_staff.id }.to_json
)
Jobs::UserEmail.new.execute(
type: :user_private_message,
user_id: suspended.id,
post_id: pm_from_staff.id,
notification_id: pm_notification.id
)
expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(
suspended.email
)
end
end
@ -195,15 +206,17 @@ describe Jobs::UserEmail do
before { SiteSetting.allow_anonymous_posting = true }
it "doesn't send email for a pm from a regular user" do
Email::Sender.any_instance.expects(:send).never
Jobs::UserEmail.new.execute(type: :user_private_message, user_id: anonymous.id, post_id: post.id)
expect(ActionMailer::Base.deliveries).to eq([])
end
it "doesn't send email for a pm from a staff user" do
pm_from_staff = Fabricate(:post, user: Fabricate(:moderator))
pm_from_staff.topic.topic_allowed_users.create!(user_id: anonymous.id)
Email::Sender.any_instance.expects(:send).never
Jobs::UserEmail.new.execute(type: :user_private_message, user_id: anonymous.id, post_id: pm_from_staff.id)
expect(ActionMailer::Base.deliveries).to eq([])
end
end
end
@ -244,17 +257,65 @@ describe Jobs::UserEmail do
end
it "does send the email if the notification has been seen but the user is set for email_always" do
Email::Sender.any_instance.expects(:send)
notification.update_column(:read, true)
user.user_option.update_column(:email_always, true)
Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id)
Jobs::UserEmail.new.execute(
type: :user_mentioned,
user_id: user.id,
post_id: post.id,
notification_id: notification.id
)
expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(
user.email
)
end
it "does send the email if the user is using daily mailing list mode" do
Email::Sender.any_instance.expects(:send)
user.user_option.update(mailing_list_mode: true, mailing_list_mode_frequency: 0)
Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id)
Jobs::UserEmail.new.execute(
type: :user_mentioned,
user_id: user.id,
post_id: post.id,
notification_id: notification.id
)
expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(
user.email
)
end
context "recently seen" do
it "doesn't send an email to a user that's been recently seen" do
user.update!(last_seen_at: 9.minutes.ago)
Jobs::UserEmail.new.execute(
type: :user_replied,
user_id: user.id,
post_id: post.id,
notification_id: notification.id
)
expect(ActionMailer::Base.deliveries).to eq([])
end
it "does send an email to a user that's been recently seen but has email_always set" do
user.update!(last_seen_at: 9.minutes.ago)
user.user_option.update!(email_always: true)
Jobs::UserEmail.new.execute(
type: :user_replied,
user_id: user.id,
post_id: post.id,
notification_id: notification.id
)
expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(
user.email
)
end
end
context 'max_emails_per_day_per_user limit is reached' do
@ -361,7 +422,6 @@ describe Jobs::UserEmail do
end
it "doesn't send the mail if the user is using individual mailing list mode" do
Email::Sender.any_instance.expects(:send).never
user.user_option.update(mailing_list_mode: true, mailing_list_mode_frequency: 1)
# sometimes, we pass the notification_id
Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id, post_id: post.id)
@ -373,10 +433,11 @@ describe Jobs::UserEmail do
post = Fabricate(:post)
post.topic.destroy
Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_type: "posted", post_id: post.id)
expect(ActionMailer::Base.deliveries).to eq([])
end
it "doesn't send the mail if the user is using individual mailing list mode with no echo" do
Email::Sender.any_instance.expects(:send).never
user.user_option.update(mailing_list_mode: true, mailing_list_mode_frequency: 2)
# sometimes, we pass the notification_id
Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id, post_id: post.id)
@ -388,12 +449,15 @@ describe Jobs::UserEmail do
post = Fabricate(:post)
post.topic.destroy
Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_type: "posted", post_id: post.id)
expect(ActionMailer::Base.deliveries).to eq([])
end
it "doesn't send the email if the post has been user deleted" do
Email::Sender.any_instance.expects(:send).never
post.update_column(:user_deleted, true)
Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id, post_id: post.id)
expect(ActionMailer::Base.deliveries).to eq([])
end
context 'user is suspended' do
@ -450,8 +514,14 @@ describe Jobs::UserEmail do
before { SiteSetting.allow_anonymous_posting = true }
it "doesn't send email for a pm from a regular user" do
Email::Sender.any_instance.expects(:send).never
Jobs::UserEmail.new.execute(type: :user_private_message, user_id: anonymous.id, notification_id: notification.id)
Jobs::UserEmail.new.execute(
type: :user_private_message,
user_id: anonymous.id,
post_id: post.id,
notification_id: notification.id
)
expect(ActionMailer::Base.deliveries).to eq([])
end
it "doesn't send email for a pm from staff" do
@ -463,8 +533,14 @@ describe Jobs::UserEmail do
post_number: pm_from_staff.post_number,
data: { original_post_id: pm_from_staff.id }.to_json
)
Email::Sender.any_instance.expects(:send).never
Jobs::UserEmail.new.execute(type: :user_private_message, user_id: anonymous.id, notification_id: pm_notification.id)
Jobs::UserEmail.new.execute(
type: :user_private_message,
user_id: anonymous.id,
post_id: pm_from_staff.id,
notification_id: pm_notification.id
)
expect(ActionMailer::Base.deliveries).to eq([])
end
end
end

View File

@ -31,7 +31,7 @@ RSpec.describe UploadRecovery do
before do
SiteSetting.authorized_extensions = 'png|pdf'
SiteSetting.queue_jobs = false
run_jobs_synchronously!
end
after do

View File

@ -68,7 +68,7 @@ describe CategoryUser do
context 'integration' do
before do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
NotificationEmailer.enable
end

View File

@ -8,7 +8,7 @@ describe DiscourseSingleSignOn do
SiteSetting.sso_url = @sso_url
SiteSetting.enable_sso = true
SiteSetting.sso_secret = @sso_secret
SiteSetting.queue_jobs = false
run_jobs_synchronously!
end
def make_sso

View File

@ -18,7 +18,7 @@ describe Emoji do
describe '.load_custom' do
describe 'when a custom emoji has an invalid upload_id' do
it 'should return the custom emoji without a URL' do
CustomEmoji.create!(name: 'test', upload_id: -1)
CustomEmoji.create!(name: 'test', upload_id: 9999)
emoji = Emoji.load_custom.first

View File

@ -1072,7 +1072,7 @@ describe PostAction do
end
it "should create a notification in the related topic" do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
post = Fabricate(:post)
user = Fabricate(:user)
action = PostAction.act(user, post, PostActionType.types[:spam], message: "WAT")
@ -1089,7 +1089,7 @@ describe PostAction do
end
it "should not add a moderator post when post is flagged via private message" do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
post = Fabricate(:post)
user = Fabricate(:user)
action = PostAction.act(user, post, PostActionType.types[:notify_user], message: "WAT")

View File

@ -41,7 +41,7 @@ describe PostMover do
before do
SiteSetting.tagging_enabled = true
SiteSetting.queue_jobs = false
run_jobs_synchronously!
p1.replies << p3
p2.replies << p4
UserActionCreator.enable
@ -570,7 +570,7 @@ describe PostMover do
before do
SiteSetting.tagging_enabled = true
SiteSetting.queue_jobs = false
run_jobs_synchronously!
p1.replies << p3
p2.replies << p4
UserActionCreator.enable

View File

@ -995,7 +995,7 @@ describe Post do
end
before do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
end
describe 'when user can not mention a group' do

View File

@ -2,7 +2,7 @@ require 'rails_helper'
describe QuotedPost do
it 'correctly extracts quotes' do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
topic = Fabricate(:topic)
post1 = create_post(topic: topic, post_number: 1, raw: "foo bar")
@ -34,7 +34,7 @@ describe QuotedPost do
end
it "doesn't count quotes from the same post" do
SiteSetting.queue_jobs = false
run_jobs_synchronously!
topic = Fabricate(:topic)
post = create_post(topic: topic, post_number: 1, raw: "foo bar")

View File

@ -1094,6 +1094,35 @@ describe Report do
include_examples "no data"
end
describe "report_top_ignored_users" do
let(:report) { Report.find("top_ignored_users") }
let(:tarek) { Fabricate(:user, username: "tarek") }
let(:john) { Fabricate(:user, username: "john") }
let(:matt) { Fabricate(:user, username: "matt") }
context "with data" do
before do
Fabricate(:ignored_user, user: tarek, ignored_user: john)
Fabricate(:ignored_user, user: tarek, ignored_user: matt)
end
it "works" do
expect(report.data.length).to eq(2)
expect_row_to_be_equal(report.data[0], john)
expect_row_to_be_equal(report.data[1], matt)
end
def expect_row_to_be_equal(row, user)
expect(row[:ignored_user_id]).to eq(user.id)
expect(row[:ignored_username]).to eq(user.username)
expect(row[:ignored_user_avatar_template]).to eq(User.avatar_template(user.username, user.uploaded_avatar_id))
expect(row[:ignores_count]).to eq(1)
end
end
include_examples "no data"
end
describe "consolidated_page_views" do
before do
freeze_time(Time.now.at_midnight)

View File

@ -150,40 +150,6 @@ describe SiteSetting do
end
end
describe '.site_home_logo_url' do
describe 'when logo site setting is set' do
let(:upload) { Fabricate(:upload) }
before do
SiteSetting.logo = upload
end
it 'should return the right URL' do
expect(SiteSetting.site_home_logo_url)
.to eq("#{Discourse.base_url}#{upload.url}")
end
end
describe 'when logo site setting is not set' do
describe 'when there is a custom title' do
before do
SiteSetting.title = "test"
end
it 'should return a blank string' do
expect(SiteSetting.site_home_logo_url).to eq('')
end
end
describe 'when title has not been set' do
it 'should return the default logo url' do
expect(SiteSetting.site_home_logo_url)
.to eq("#{Discourse.base_url}/images/d-logo-sketch.png")
end
end
end
end
context 'deprecated site settings' do
before do
SiteSetting.force_https = true

Some files were not shown because too many files have changed in this diff Show More