mirror of
https://github.com/discourse/discourse.git
synced 2025-02-02 10:19:30 +08:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
741f5f92a1
2
Gemfile
2
Gemfile
|
@ -44,7 +44,7 @@ gem 'redis-namespace'
|
|||
|
||||
gem 'active_model_serializers', '~> 0.8.3'
|
||||
|
||||
gem 'onebox', '1.8.79'
|
||||
gem 'onebox', '1.8.82'
|
||||
|
||||
gem 'http_accept_language', '~>2.0.5', require: false
|
||||
|
||||
|
|
|
@ -261,7 +261,7 @@ GEM
|
|||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.8.79)
|
||||
onebox (1.8.82)
|
||||
htmlentities (~> 4.3)
|
||||
moneta (~> 1.0)
|
||||
multi_json (~> 1.11)
|
||||
|
@ -515,7 +515,7 @@ DEPENDENCIES
|
|||
omniauth-oauth2
|
||||
omniauth-openid
|
||||
omniauth-twitter
|
||||
onebox (= 1.8.79)
|
||||
onebox (= 1.8.82)
|
||||
openid-redis-store
|
||||
pg
|
||||
pry-nav
|
||||
|
|
|
@ -136,7 +136,9 @@ export default Post.extend({
|
|||
label: I18n.t("yes_value"),
|
||||
class: "btn-danger",
|
||||
callback() {
|
||||
Post.deleteMany(replies.map(r => r.id), { deferFlags: true })
|
||||
Post.deleteMany(replies.map(r => r.id), {
|
||||
agreeWithFirstReplyFlag: false
|
||||
})
|
||||
.then(action)
|
||||
.then(resolve)
|
||||
.catch(error => {
|
||||
|
|
|
@ -47,7 +47,10 @@ export default Ember.Component.extend(
|
|||
this.set("hidden", false);
|
||||
}
|
||||
|
||||
buffer.push(`<a href='${href}'>`);
|
||||
buffer.push(
|
||||
`<a href='${href}'` + (this.get("active") ? 'class="active"' : "") + `>`
|
||||
);
|
||||
|
||||
if (content.get("hasIcon")) {
|
||||
buffer.push("<span class='" + content.get("name") + "'></span>");
|
||||
}
|
||||
|
|
|
@ -902,7 +902,7 @@ export default Ember.Controller.extend({
|
|||
composerModel.set("composeState", Composer.OPEN);
|
||||
composerModel.set("isWarning", false);
|
||||
|
||||
if (opts.usernames) {
|
||||
if (opts.usernames && !this.get("model.targetUsernames")) {
|
||||
this.set("model.targetUsernames", opts.usernames);
|
||||
}
|
||||
|
||||
|
|
|
@ -103,6 +103,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
|
||||
actions: {
|
||||
saveTimer() {
|
||||
if (!this.get("topicTimer.updateTime")) {
|
||||
this.flash(
|
||||
I18n.t("topic.topic_status_update.time_frame_required"),
|
||||
"alert-error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this._setTimer(
|
||||
this.get("topicTimer.updateTime"),
|
||||
this.get("topicTimer.status_type")
|
||||
|
|
|
@ -5,6 +5,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
|||
export default Ember.Controller.extend(PreferencesTabController, {
|
||||
saveAttrNames: [
|
||||
"muted_usernames",
|
||||
"ignored_usernames",
|
||||
"new_topic_duration_minutes",
|
||||
"auto_track_topics_after_msecs",
|
||||
"notification_level_when_replying",
|
||||
|
|
|
@ -24,7 +24,7 @@ export default Ember.Controller.extend({
|
|||
this.set("searchTerm", "");
|
||||
},
|
||||
|
||||
@observes("searchTearm")
|
||||
@observes("searchTerm")
|
||||
_searchTermChanged: debounce(function() {
|
||||
Invite.findInvitedBy(
|
||||
this.get("user"),
|
||||
|
@ -90,7 +90,6 @@ export default Ember.Controller.extend({
|
|||
Invite.rescindAll()
|
||||
.then(() => {
|
||||
this.set("rescindedAll", true);
|
||||
this.get("model.invites").clear();
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
|
|
|
@ -46,11 +46,26 @@ export function translateResults(results, opts) {
|
|||
|
||||
results.groups = results.groups
|
||||
.map(group => {
|
||||
const groupName = Handlebars.Utils.escapeExpression(group.name);
|
||||
const name = Handlebars.Utils.escapeExpression(group.name);
|
||||
const fullName = Handlebars.Utils.escapeExpression(
|
||||
group.full_name || group.display_name
|
||||
);
|
||||
const flairUrl = Ember.isEmpty(group.flair_url)
|
||||
? null
|
||||
: Handlebars.Utils.escapeExpression(group.flair_url);
|
||||
const flairColor = Handlebars.Utils.escapeExpression(group.flair_color);
|
||||
const flairBgColor = Handlebars.Utils.escapeExpression(
|
||||
group.flair_bg_color
|
||||
);
|
||||
|
||||
return {
|
||||
id: group.id,
|
||||
name: groupName,
|
||||
url: Discourse.getURL(`/g/${groupName}`)
|
||||
flairUrl,
|
||||
flairColor,
|
||||
flairBgColor,
|
||||
fullName,
|
||||
name,
|
||||
url: Discourse.getURL(`/g/${name}`)
|
||||
};
|
||||
})
|
||||
.compact();
|
||||
|
@ -72,10 +87,10 @@ export function translateResults(results, opts) {
|
|||
if (groupedSearchResult) {
|
||||
[
|
||||
["topic", "posts"],
|
||||
["category", "categories"],
|
||||
["tag", "tags"],
|
||||
["user", "users"],
|
||||
["group", "groups"]
|
||||
["group", "groups"],
|
||||
["category", "categories"],
|
||||
["tag", "tags"]
|
||||
].forEach(function(pair) {
|
||||
const type = pair[0];
|
||||
const name = pair[1];
|
||||
|
|
|
@ -80,6 +80,7 @@ export function transformBasicPost(post) {
|
|||
expandablePost: false,
|
||||
replyCount: post.reply_count,
|
||||
locked: post.locked,
|
||||
ignored: post.ignored,
|
||||
userCustomFields: post.user_custom_fields
|
||||
};
|
||||
|
||||
|
@ -133,6 +134,13 @@ export default function transformPost(
|
|||
postAtts.topicUrl = topic.get("url");
|
||||
postAtts.isSaving = post.isSaving;
|
||||
|
||||
if (post.post_notice_type) {
|
||||
postAtts.postNoticeType = post.post_notice_type;
|
||||
if (postAtts.postNoticeType === "returning") {
|
||||
postAtts.postNoticeTime = new Date(post.post_notice_time);
|
||||
}
|
||||
}
|
||||
|
||||
const showPMMap =
|
||||
topic.archetype === "private_message" && post.post_number === 1;
|
||||
if (showPMMap) {
|
||||
|
|
|
@ -378,10 +378,10 @@ Post.reopenClass({
|
|||
});
|
||||
},
|
||||
|
||||
deleteMany(post_ids, { deferFlags = false } = {}) {
|
||||
deleteMany(post_ids, { agreeWithFirstReplyFlag = true } = {}) {
|
||||
return ajax("/posts/destroy_many", {
|
||||
type: "DELETE",
|
||||
data: { post_ids, defer_flags: deferFlags }
|
||||
data: { post_ids, agree_with_first_reply_flag: agreeWithFirstReplyFlag }
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -249,6 +249,7 @@ const User = RestModel.extend({
|
|||
"custom_fields",
|
||||
"user_fields",
|
||||
"muted_usernames",
|
||||
"ignored_usernames",
|
||||
"profile_background",
|
||||
"card_background",
|
||||
"muted_tags",
|
||||
|
|
|
@ -48,6 +48,8 @@ export default Discourse.Route.extend({
|
|||
}
|
||||
})
|
||||
.catch(() => bootbox.alert(I18n.t("generic_error")));
|
||||
} else {
|
||||
e.send("createNewMessageViaParams", null, params.title, params.body);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -19,6 +19,11 @@
|
|||
<p>{{model.description}}</p>
|
||||
</section>
|
||||
|
||||
{{plugin-outlet name="about-after-description"
|
||||
connectorTagName='section'
|
||||
tagName=''
|
||||
args=(hash model=model)}}
|
||||
|
||||
{{#if model.admins}}
|
||||
<section class='about admins'>
|
||||
<h3>{{d-icon "users"}} {{i18n 'about.our_admins'}}</h3>
|
||||
|
|
|
@ -1 +1 @@
|
|||
<a href {{action "select"}}>{{title}}</a>
|
||||
<a href {{action "select"}} class="{{if active 'active'}}">{{title}}</a>
|
||||
|
|
|
@ -41,11 +41,11 @@
|
|||
<figure title="{{i18n 'all_time_desc'}}">{{number c.topics_all_time}} <figcaption>{{i18n 'all_time'}}</figcaption></figure>
|
||||
|
||||
{{#if c.pickMonth}}
|
||||
<figure title="{{i18n 'month_desc'}}">{{number c.topics_month}} <figcaption>{{i18n 'month'}}</figcaption></figure>
|
||||
<figure title="{{i18n 'month_desc'}}">{{number c.topics_month}} <figcaption>/ {{i18n 'month'}}</figcaption></figure>
|
||||
{{/if}}
|
||||
|
||||
{{#if c.pickWeek}}
|
||||
<figure title="{{i18n 'week_desc'}}">{{number c.topics_week}} <figcaption>{{i18n 'week'}}</figcaption></figure>
|
||||
<figure title="{{i18n 'week_desc'}}">{{number c.topics_week}} <figcaption>/ {{i18n 'week'}}</figcaption></figure>
|
||||
{{/if}}
|
||||
|
||||
</footer>
|
||||
|
|
|
@ -81,19 +81,19 @@
|
|||
{{else}}
|
||||
<td>{{unbound invite.email}}</td>
|
||||
<td>{{format-date invite.created_at}}</td>
|
||||
<td colspan='5'>
|
||||
<td>
|
||||
{{#if invite.expired}}
|
||||
{{i18n 'user.invited.expired'}}
|
||||
|
||||
<div>{{i18n 'user.invited.expired'}}</div>
|
||||
{{/if}}
|
||||
{{#if invite.rescinded}}
|
||||
{{i18n 'user.invited.rescinded'}}
|
||||
{{else}}
|
||||
{{d-button icon="times" action=(action "rescind") actionParam=invite label="user.invited.rescind"}}
|
||||
{{/if}}
|
||||
|
||||
</td>
|
||||
<td>
|
||||
{{#if invite.reinvited}}
|
||||
{{i18n 'user.invited.reinvited'}}
|
||||
<div>{{i18n 'user.invited.reinvited'}}</div>
|
||||
{{else}}
|
||||
{{d-button icon="sync" action=(action "reinvite") actionParam=invite label="user.invited.reinvite"}}
|
||||
{{/if}}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
formatUsername
|
||||
} from "discourse/lib/utilities";
|
||||
import hbs from "discourse/widgets/hbs-compiler";
|
||||
import { relativeAge } from "discourse/lib/formatter";
|
||||
|
||||
function transformWithCallbacks(post) {
|
||||
let transformed = transformBasicPost(post);
|
||||
|
@ -427,6 +428,29 @@ createWidget("post-contents", {
|
|||
}
|
||||
});
|
||||
|
||||
createWidget("post-notice", {
|
||||
tagName: "div.post-notice",
|
||||
|
||||
html(attrs) {
|
||||
let text, icon;
|
||||
if (attrs.postNoticeType === "first") {
|
||||
icon = "hands-helping";
|
||||
text = I18n.t("post.notice.first", { user: attrs.username });
|
||||
} else if (attrs.postNoticeType === "returning") {
|
||||
icon = "far-smile";
|
||||
text = I18n.t("post.notice.return", {
|
||||
user: attrs.username,
|
||||
time: relativeAge(attrs.postNoticeTime, {
|
||||
format: "tiny",
|
||||
addAgo: true
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return h("p", [iconNode(icon), text]);
|
||||
}
|
||||
});
|
||||
|
||||
createWidget("post-body", {
|
||||
tagName: "div.topic-body.clearfix",
|
||||
|
||||
|
@ -505,6 +529,10 @@ createWidget("post-article", {
|
|||
);
|
||||
}
|
||||
|
||||
if (attrs.postNoticeType) {
|
||||
rows.push(h("div.row", [this.attach("post-notice", attrs)]));
|
||||
}
|
||||
|
||||
rows.push(
|
||||
h("div.row", [
|
||||
this.attach("post-avatar", attrs),
|
||||
|
@ -608,6 +636,9 @@ export default createWidget("post", {
|
|||
} else {
|
||||
classNames.push("regular");
|
||||
}
|
||||
if (attrs.ignored) {
|
||||
classNames.push("post-ignored");
|
||||
}
|
||||
if (addPostClassesCallbacks) {
|
||||
for (let i = 0; i < addPostClassesCallbacks.length; i++) {
|
||||
let pluginClasses = addPostClassesCallbacks[i].call(this, attrs);
|
||||
|
|
|
@ -152,14 +152,17 @@ export default createWidget("private-message-map", {
|
|||
}
|
||||
|
||||
const result = [h(`div.participants${hideNamesClass}`, participants)];
|
||||
const controls = [];
|
||||
|
||||
const controls = [
|
||||
this.attach("button", {
|
||||
action: "toggleEditing",
|
||||
label: "private_message_info.edit",
|
||||
className: "btn btn-default add-remove-participant-btn"
|
||||
})
|
||||
];
|
||||
if (attrs.canRemoveAllowedUsers || attrs.canRemoveSelfId) {
|
||||
controls.push(
|
||||
this.attach("button", {
|
||||
action: "toggleEditing",
|
||||
label: "private_message_info.edit",
|
||||
className: "btn btn-default add-remove-participant-btn"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (attrs.canInvite && this.state.isEditing) {
|
||||
controls.push(
|
||||
|
@ -171,7 +174,9 @@ export default createWidget("private-message-map", {
|
|||
);
|
||||
}
|
||||
|
||||
result.push(h("div.controls", controls));
|
||||
if (controls.length) {
|
||||
result.push(h("div.controls", controls));
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
|
|
@ -90,16 +90,48 @@ createSearchResult({
|
|||
}
|
||||
});
|
||||
|
||||
createSearchResult({
|
||||
type: "group",
|
||||
linkField: "url",
|
||||
builder(group) {
|
||||
const fullName = escapeExpression(group.fullName);
|
||||
const name = escapeExpression(group.name);
|
||||
const groupNames = [h("span.name", fullName || name)];
|
||||
|
||||
if (fullName) {
|
||||
groupNames.push(h("span.slug", name));
|
||||
}
|
||||
|
||||
let avatarFlair;
|
||||
if (group.flairUrl) {
|
||||
avatarFlair = this.attach("avatar-flair", {
|
||||
primary_group_flair_url: group.flairUrl,
|
||||
primary_group_flair_bg_color: group.flairBgColor,
|
||||
primary_group_flair_color: group.flairColor,
|
||||
primary_group_name: name
|
||||
});
|
||||
} else {
|
||||
avatarFlair = iconNode("users");
|
||||
}
|
||||
|
||||
const groupResultContents = [avatarFlair, h("div.group-names", groupNames)];
|
||||
|
||||
return h("div.group-result", groupResultContents);
|
||||
}
|
||||
});
|
||||
|
||||
createSearchResult({
|
||||
type: "user",
|
||||
linkField: "path",
|
||||
builder(u) {
|
||||
const userTitles = [h("span.username", formatUsername(u.username))];
|
||||
const userTitles = [];
|
||||
|
||||
if (u.name) {
|
||||
userTitles.push(h("span.name", u.name));
|
||||
}
|
||||
|
||||
userTitles.push(h("span.username", formatUsername(u.username)));
|
||||
|
||||
const userResultContents = [
|
||||
avatarImg("small", {
|
||||
template: u.avatar_template,
|
||||
|
@ -112,21 +144,6 @@ createSearchResult({
|
|||
}
|
||||
});
|
||||
|
||||
createSearchResult({
|
||||
type: "group",
|
||||
linkField: "url",
|
||||
builder(group) {
|
||||
const groupName = escapeExpression(group.name);
|
||||
return h(
|
||||
"span",
|
||||
{
|
||||
className: `group-${groupName} discourse-group`
|
||||
},
|
||||
[iconNode("users"), h("span", groupName)]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
createSearchResult({
|
||||
type: "topic",
|
||||
linkField: "url",
|
||||
|
@ -174,19 +191,12 @@ createWidget("search-menu-results", {
|
|||
const resultTypes = results.resultTypes || [];
|
||||
|
||||
const mainResultsContent = [];
|
||||
const classificationContents = [];
|
||||
const otherContents = [];
|
||||
const assignContainer = (type, node) => {
|
||||
if (["topic"].includes(type)) {
|
||||
mainResultsContent.push(node);
|
||||
} else if (["category", "tag"].includes(type)) {
|
||||
classificationContents.push(node);
|
||||
} else {
|
||||
otherContents.push(node);
|
||||
}
|
||||
};
|
||||
const usersAndGroups = [];
|
||||
const categoriesAndTags = [];
|
||||
const usersAndGroupsMore = [];
|
||||
const categoriesAndTagsMore = [];
|
||||
|
||||
resultTypes.forEach(rt => {
|
||||
const buildMoreNode = result => {
|
||||
const more = [];
|
||||
|
||||
const moreArgs = {
|
||||
|
@ -194,23 +204,45 @@ createWidget("search-menu-results", {
|
|||
contents: () => [I18n.t("more"), "..."]
|
||||
};
|
||||
|
||||
if (rt.moreUrl) {
|
||||
if (result.moreUrl) {
|
||||
more.push(
|
||||
this.attach("link", $.extend(moreArgs, { href: rt.moreUrl }))
|
||||
this.attach("link", $.extend(moreArgs, { href: result.moreUrl }))
|
||||
);
|
||||
} else if (rt.more) {
|
||||
} else if (result.more) {
|
||||
more.push(
|
||||
this.attach(
|
||||
"link",
|
||||
$.extend(moreArgs, {
|
||||
action: "moreOfType",
|
||||
actionParam: rt.type,
|
||||
actionParam: result.type,
|
||||
className: "filter filter-type"
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (more.length) {
|
||||
return more;
|
||||
}
|
||||
};
|
||||
|
||||
const assignContainer = (result, node) => {
|
||||
if (["topic"].includes(result.type)) {
|
||||
mainResultsContent.push(node);
|
||||
}
|
||||
|
||||
if (["user", "group"].includes(result.type)) {
|
||||
usersAndGroups.push(node);
|
||||
usersAndGroupsMore.push(buildMoreNode(result));
|
||||
}
|
||||
|
||||
if (["category", "tag"].includes(result.type)) {
|
||||
categoriesAndTags.push(node);
|
||||
categoriesAndTagsMore.push(buildMoreNode(result));
|
||||
}
|
||||
};
|
||||
|
||||
resultTypes.forEach(rt => {
|
||||
const resultNodeContents = [
|
||||
this.attach(rt.componentName, {
|
||||
searchContextEnabled: attrs.searchContextEnabled,
|
||||
|
@ -220,14 +252,14 @@ createWidget("search-menu-results", {
|
|||
})
|
||||
];
|
||||
|
||||
if (more.length) {
|
||||
resultNodeContents.push(h("div.show-more", more));
|
||||
if (["topic"].includes(rt.type)) {
|
||||
const more = buildMoreNode(rt);
|
||||
if (more) {
|
||||
resultNodeContents.push(h("div.show-more", more));
|
||||
}
|
||||
}
|
||||
|
||||
assignContainer(
|
||||
rt.type,
|
||||
h(`div.${rt.componentName}`, resultNodeContents)
|
||||
);
|
||||
assignContainer(rt, h(`div.${rt.componentName}`, resultNodeContents));
|
||||
});
|
||||
|
||||
const content = [];
|
||||
|
@ -236,27 +268,25 @@ createWidget("search-menu-results", {
|
|||
content.push(h("div.main-results", mainResultsContent));
|
||||
}
|
||||
|
||||
if (classificationContents.length || otherContents.length) {
|
||||
const secondaryResultsContent = [];
|
||||
if (usersAndGroups.length || categoriesAndTags.length) {
|
||||
const secondaryResultsContents = [];
|
||||
|
||||
if (classificationContents.length) {
|
||||
secondaryResultsContent.push(
|
||||
h("div.classification-results", classificationContents)
|
||||
);
|
||||
secondaryResultsContents.push(usersAndGroups);
|
||||
secondaryResultsContents.push(usersAndGroupsMore);
|
||||
|
||||
if (usersAndGroups.length && categoriesAndTags.length) {
|
||||
secondaryResultsContents.push(h("div.separator"));
|
||||
}
|
||||
|
||||
if (otherContents.length) {
|
||||
secondaryResultsContent.push(h("div.other-results", otherContents));
|
||||
}
|
||||
secondaryResultsContents.push(categoriesAndTags);
|
||||
secondaryResultsContents.push(categoriesAndTagsMore);
|
||||
|
||||
content.push(
|
||||
h(
|
||||
`div.secondary-results${
|
||||
mainResultsContent.length ? "" : ".no-main-results"
|
||||
}`,
|
||||
secondaryResultsContent
|
||||
)
|
||||
const secondaryResults = h(
|
||||
"div.secondary-results",
|
||||
secondaryResultsContents
|
||||
);
|
||||
|
||||
content.push(secondaryResults);
|
||||
}
|
||||
|
||||
return content;
|
||||
|
|
|
@ -428,6 +428,11 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
a.active {
|
||||
background: $primary-medium;
|
||||
color: $secondary;
|
||||
}
|
||||
|
||||
a.blank:not(.active) {
|
||||
color: $primary-medium;
|
||||
}
|
||||
|
|
|
@ -76,26 +76,22 @@
|
|||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
|
||||
.classification-results {
|
||||
border-bottom: 1px solid $primary-low;
|
||||
.separator {
|
||||
margin-bottom: 1em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.search-result-category {
|
||||
margin-top: 1em;
|
||||
height: 1px;
|
||||
background: $primary-low;
|
||||
}
|
||||
|
||||
.search-result-tag {
|
||||
.list {
|
||||
.item {
|
||||
display: inline-flex;
|
||||
.discourse-tag {
|
||||
font-size: $font-down-1;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-link.search-link {
|
||||
display: inline;
|
||||
font-size: $font-0;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
.search-result-category {
|
||||
.widget-link {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,12 +104,71 @@
|
|||
}
|
||||
}
|
||||
|
||||
.discourse-group {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
.group-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.d-icon {
|
||||
margin-right: s(1);
|
||||
.d-icon,
|
||||
.avatar-flair {
|
||||
min-width: 25px;
|
||||
margin-right: 0.5em;
|
||||
|
||||
.d-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-flair-image {
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
min-height: 25px;
|
||||
}
|
||||
|
||||
.group-names {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
line-height: $line-height-medium;
|
||||
|
||||
&:hover {
|
||||
.name,
|
||||
.slug {
|
||||
color: $primary-high;
|
||||
}
|
||||
}
|
||||
|
||||
.name,
|
||||
.slug {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.slug {
|
||||
font-size: $font-down-1;
|
||||
color: $primary-high;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-result-category,
|
||||
.search-result-user,
|
||||
.search-result-group,
|
||||
.search-result-tag {
|
||||
.list {
|
||||
display: block;
|
||||
|
||||
.item {
|
||||
.widget-link.search-link {
|
||||
flex: 1;
|
||||
font-size: $font-0;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -145,29 +200,17 @@
|
|||
|
||||
.username {
|
||||
color: dark-light-choose($primary-high, $secondary-low);
|
||||
font-size: $font-0;
|
||||
font-weight: 700;
|
||||
font-size: $font-down-1;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: dark-light-choose($primary-high, $secondary-low);
|
||||
font-size: $font-down-1;
|
||||
font-size: $font-0;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.no-main-results .search-result-user {
|
||||
.user-titles {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
margin: 0 0 0 0.25em;
|
||||
font-size: $font-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.show-more {
|
||||
|
|
|
@ -214,6 +214,10 @@ aside.quote {
|
|||
margin: -2px;
|
||||
}
|
||||
|
||||
.post-ignored {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.post-action {
|
||||
.undo-action,
|
||||
.act-action {
|
||||
|
@ -353,7 +357,10 @@ aside.quote {
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
& + .controls {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
&.hide-names .user {
|
||||
.username,
|
||||
|
@ -857,3 +864,22 @@ a.mention-group {
|
|||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.post-notice {
|
||||
background-color: $tertiary-low;
|
||||
border-top: 1px solid $primary-low;
|
||||
color: $primary;
|
||||
padding: 1em;
|
||||
max-width: calc(
|
||||
#{$topic-body-width} + #{$topic-avatar-width} - #{$topic-body-width-padding} +
|
||||
3px
|
||||
);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.d-icon {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,8 +48,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.active > a,
|
||||
> a.active {
|
||||
a.active {
|
||||
color: $secondary;
|
||||
background-color: $quaternary;
|
||||
|
||||
|
|
|
@ -203,8 +203,24 @@
|
|||
border: 1px solid $primary-low;
|
||||
}
|
||||
|
||||
.d-editor-preview img {
|
||||
padding-bottom: 1.4em;
|
||||
&.emoji,
|
||||
&.avatar,
|
||||
&.site-icon {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.d-editor-preview .image-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding-bottom: 1.4em;
|
||||
|
||||
img {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.button-wrapper {
|
||||
opacity: 0.9;
|
||||
|
@ -212,21 +228,22 @@
|
|||
}
|
||||
.button-wrapper {
|
||||
opacity: 0;
|
||||
background: $secondary;
|
||||
position: absolute;
|
||||
transition: all 0.25s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
bottom: 0.75em;
|
||||
left: 0.75em;
|
||||
box-shadow: shadow("dropdown");
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
.separator {
|
||||
color: $primary-low;
|
||||
color: $primary-low-mid;
|
||||
}
|
||||
|
||||
.scale-btn {
|
||||
color: $tertiary;
|
||||
padding: 0.2em 0.6em;
|
||||
padding: 0 0.4em;
|
||||
&:first-of-type {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
|
|
|
@ -24,8 +24,6 @@
|
|||
z-index: z("dropdown");
|
||||
|
||||
.select-kit-body {
|
||||
-webkit-animation: fadein 0.25s;
|
||||
animation: fadein 0.25s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
left: 0;
|
||||
|
|
|
@ -47,60 +47,69 @@ section.post-menu-area {
|
|||
nav.post-controls {
|
||||
padding: 0;
|
||||
.like-button {
|
||||
// Like button wrapper
|
||||
display: inline-flex;
|
||||
.like-count {
|
||||
color: dark-light-choose($primary-low-mid, $secondary-high);
|
||||
}
|
||||
.widget-button {
|
||||
background: none;
|
||||
}
|
||||
color: $primary-low-mid;
|
||||
margin-right: 0.15em;
|
||||
&:hover {
|
||||
background: $primary-low;
|
||||
.like-count {
|
||||
// Like button wrapper on hover
|
||||
button {
|
||||
background: $primary-low;
|
||||
color: $primary-medium;
|
||||
}
|
||||
}
|
||||
button {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
&.my-likes {
|
||||
// Like count on my posts
|
||||
.d-icon {
|
||||
color: $primary-low-mid;
|
||||
padding-left: 0.45em;
|
||||
}
|
||||
}
|
||||
&.like {
|
||||
// Like button with 0 likes
|
||||
&.d-hover {
|
||||
background: $love-low;
|
||||
.d-icon {
|
||||
color: $love;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.has-like {
|
||||
// Like button after I've liked
|
||||
.d-icon {
|
||||
color: $love;
|
||||
}
|
||||
&.d-hover {
|
||||
background: $primary-low;
|
||||
.d-icon {
|
||||
color: $primary-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
&[disabled] {
|
||||
// Disabled like button
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&.like-count {
|
||||
// Like count button
|
||||
&:not(.my-likes) {
|
||||
padding-right: 0;
|
||||
}
|
||||
&.d-hover {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
.d-hover {
|
||||
background: none;
|
||||
}
|
||||
.d-icon {
|
||||
color: $love;
|
||||
+ .toggle-like {
|
||||
// Like button when like count is present
|
||||
padding-left: 0.45em;
|
||||
&.d-hover {
|
||||
background: $primary-low;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4);
|
||||
.widget-button {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
.like {
|
||||
&:focus {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
.like-count {
|
||||
font-size: $font-up-1;
|
||||
margin-left: 0;
|
||||
.d-icon {
|
||||
padding-left: 10px;
|
||||
color: dark-light-choose($primary-low-mid, $secondary-high);
|
||||
}
|
||||
&.my-likes {
|
||||
margin-right: -2px;
|
||||
}
|
||||
&.regular-likes {
|
||||
margin-right: -12px;
|
||||
}
|
||||
}
|
||||
.toggle-like {
|
||||
padding: 8px 8px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
.highlight-action {
|
||||
color: dark-light-choose($primary-medium, $secondary-high);
|
||||
}
|
||||
a,
|
||||
button {
|
||||
|
@ -186,23 +195,6 @@ nav.post-controls {
|
|||
color: $secondary;
|
||||
}
|
||||
}
|
||||
&.like.d-hover,
|
||||
&.like:focus {
|
||||
color: $love;
|
||||
background: $love-low;
|
||||
.d-icon {
|
||||
color: $love;
|
||||
}
|
||||
}
|
||||
&.has-like .d-icon {
|
||||
color: $love;
|
||||
}
|
||||
&.has-like[disabled]:hover {
|
||||
background: transparent;
|
||||
}
|
||||
&.has-like[disabled]:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
&.bookmark {
|
||||
padding: 8px 11px;
|
||||
&.bookmarked .d-icon {
|
||||
|
|
|
@ -115,6 +115,11 @@
|
|||
.user-invite-list {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
tr {
|
||||
td {
|
||||
padding: 0.667em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-invite-search {
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
|
||||
form {
|
||||
margin-top: 20px;
|
||||
input[type="text"] {
|
||||
input:not(.filter-input)[type="text"] {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -475,3 +475,7 @@ span.highlighted {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.post-notice {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
|
|
@ -113,12 +113,15 @@ class Admin::EmailTemplatesController < Admin::AdminController
|
|||
|
||||
def update_key(key, value)
|
||||
old_value = I18n.t(key)
|
||||
translation_override = TranslationOverride.upsert!(I18n.locale, key, value)
|
||||
|
||||
unless old_value.is_a?(Hash)
|
||||
translation_override = TranslationOverride.upsert!(I18n.locale, key, value)
|
||||
end
|
||||
|
||||
{
|
||||
key: key,
|
||||
old_value: old_value,
|
||||
error_messages: translation_override.errors.full_messages
|
||||
error_messages: translation_override&.errors&.full_messages
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -143,7 +143,7 @@ class InvitesController < ApplicationController
|
|||
def rescind_all_invites
|
||||
guardian.ensure_can_rescind_all_invites!(current_user)
|
||||
|
||||
Invite.rescind_all_invites_from(current_user)
|
||||
Invite.rescind_all_expired_invites_from(current_user)
|
||||
render body: nil
|
||||
end
|
||||
|
||||
|
|
|
@ -336,7 +336,7 @@ class PostsController < ApplicationController
|
|||
|
||||
def destroy_many
|
||||
params.require(:post_ids)
|
||||
defer_flags = params[:defer_flags] || false
|
||||
agree_with_first_reply_flag = (params[:agree_with_first_reply_flag] || true).to_s == "true"
|
||||
|
||||
posts = Post.where(id: post_ids_including_replies)
|
||||
raise Discourse::InvalidParameters.new(:post_ids) if posts.blank?
|
||||
|
@ -345,7 +345,9 @@ class PostsController < ApplicationController
|
|||
posts.each { |p| guardian.ensure_can_delete!(p) }
|
||||
|
||||
Post.transaction do
|
||||
posts.each { |p| PostDestroyer.new(current_user, p, defer_flags: defer_flags).destroy }
|
||||
posts.each_with_index do |p, i|
|
||||
PostDestroyer.new(current_user, p, defer_flags: !(agree_with_first_reply_flag && i == 0)).destroy
|
||||
end
|
||||
end
|
||||
|
||||
render body: nil
|
||||
|
|
|
@ -1226,6 +1226,7 @@ class UsersController < ApplicationController
|
|||
:title,
|
||||
:date_of_birth,
|
||||
:muted_usernames,
|
||||
:ignored_usernames,
|
||||
:theme_ids,
|
||||
:locale,
|
||||
:bio_raw,
|
||||
|
|
102
app/jobs/base.rb
102
app/jobs/base.rb
|
@ -17,63 +17,74 @@ module Jobs
|
|||
|
||||
class Base
|
||||
class JobInstrumenter
|
||||
def initialize(job_class:, opts:, db:)
|
||||
def initialize(job_class:, opts:, db:, jid:)
|
||||
return unless enabled?
|
||||
@data = {}
|
||||
self.class.mutex.synchronize do
|
||||
@data = {}
|
||||
|
||||
@data["hostname"] = `hostname`.strip # Hostname
|
||||
@data["pid"] = Process.pid # Pid
|
||||
@data["database"] = db # DB name - multisite db name it ran on
|
||||
@data["job_name"] = job_class.name # Job Name - eg: Jobs::AboutStats
|
||||
@data["job_type"] = job_class.try(:scheduled?) ? "scheduled" : "regular" # Job Type - either s for scheduled or r for regular
|
||||
@data["opts"] = opts.to_json # Params - json encoded params for the job
|
||||
@data["hostname"] = `hostname`.strip # Hostname
|
||||
@data["pid"] = Process.pid # Pid
|
||||
@data["database"] = db # DB name - multisite db name it ran on
|
||||
@data["job_id"] = jid # Job unique ID
|
||||
@data["job_name"] = job_class.name # Job Name - eg: Jobs::AboutStats
|
||||
@data["job_type"] = job_class.try(:scheduled?) ? "scheduled" : "regular" # Job Type - either s for scheduled or r for regular
|
||||
@data["opts"] = opts.to_json # Params - json encoded params for the job
|
||||
|
||||
@data["status"] = 'pending'
|
||||
@start_timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
if ENV["DISCOURSE_LOG_SIDEKIQ_INTERVAL"]
|
||||
@data["status"] = "starting"
|
||||
write_to_log
|
||||
end
|
||||
|
||||
self.class.ensure_interval_logging!
|
||||
@@active_jobs ||= []
|
||||
@@active_jobs << self
|
||||
@data["status"] = "pending"
|
||||
@start_timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
|
||||
MethodProfiler.ensure_discourse_instrumentation!
|
||||
MethodProfiler.start
|
||||
self.class.ensure_interval_logging!
|
||||
@@active_jobs ||= []
|
||||
@@active_jobs << self
|
||||
|
||||
MethodProfiler.ensure_discourse_instrumentation!
|
||||
MethodProfiler.start
|
||||
end
|
||||
end
|
||||
|
||||
def stop(exception:)
|
||||
return unless enabled?
|
||||
self.class.mutex.synchronize do
|
||||
profile = MethodProfiler.stop
|
||||
|
||||
profile = MethodProfiler.stop
|
||||
@@active_jobs.delete(self)
|
||||
|
||||
@@active_jobs.delete(self)
|
||||
@data["duration"] = profile[:total_duration] # Duration - length in seconds it took to run
|
||||
@data["sql_duration"] = profile.dig(:sql, :duration) || 0 # Sql Duration (s)
|
||||
@data["sql_calls"] = profile.dig(:sql, :calls) || 0 # Sql Statements - how many statements ran
|
||||
@data["redis_duration"] = profile.dig(:redis, :duration) || 0 # Redis Duration (s)
|
||||
@data["redis_calls"] = profile.dig(:redis, :calls) || 0 # Redis commands
|
||||
@data["net_duration"] = profile.dig(:net, :duration) || 0 # Redis Duration (s)
|
||||
@data["net_calls"] = profile.dig(:net, :calls) || 0 # Redis commands
|
||||
|
||||
@data["duration"] = profile[:total_duration] # Duration - length in seconds it took to run
|
||||
@data["sql_duration"] = profile.dig(:sql, :duration) || 0 # Sql Duration (s)
|
||||
@data["sql_calls"] = profile.dig(:sql, :calls) || 0 # Sql Statements - how many statements ran
|
||||
@data["redis_duration"] = profile.dig(:redis, :duration) || 0 # Redis Duration (s)
|
||||
@data["redis_calls"] = profile.dig(:redis, :calls) || 0 # Redis commands
|
||||
@data["net_duration"] = profile.dig(:net, :duration) || 0 # Redis Duration (s)
|
||||
@data["net_calls"] = profile.dig(:net, :calls) || 0 # Redis commands
|
||||
if exception.present?
|
||||
@data["exception"] = exception # Exception - if job fails a json encoded exception
|
||||
@data["status"] = 'failed'
|
||||
else
|
||||
@data["status"] = 'success' # Status - fail, success, pending
|
||||
end
|
||||
|
||||
if exception.present?
|
||||
@data["exception"] = exception # Exception - if job fails a json encoded exception
|
||||
@data["status"] = 'failed'
|
||||
else
|
||||
@data["status"] = 'success' # Status - fail, success, pending
|
||||
write_to_log
|
||||
end
|
||||
|
||||
write_to_log
|
||||
end
|
||||
|
||||
def self.raw_log(message)
|
||||
@@logger ||= Logger.new("#{Rails.root}/log/sidekiq.log")
|
||||
@@logger ||= begin
|
||||
f = File.open "#{Rails.root}/log/sidekiq.log", "a"
|
||||
f.sync = true
|
||||
Logger.new f
|
||||
end
|
||||
@@log_queue ||= Queue.new
|
||||
unless @log_thread&.alive?
|
||||
@@log_thread = Thread.new do
|
||||
begin
|
||||
loop { @@logger << @@log_queue.pop }
|
||||
rescue Exception => e
|
||||
Discourse.warn_exception(e, message: "Sidekiq logging thread terminated unexpectedly")
|
||||
end
|
||||
@@log_thread ||= Thread.new do
|
||||
begin
|
||||
loop { @@logger << @@log_queue.pop }
|
||||
rescue Exception => e
|
||||
Discourse.warn_exception(e, message: "Sidekiq logging thread terminated unexpectedly")
|
||||
end
|
||||
end
|
||||
@@log_queue.push(message)
|
||||
|
@ -94,14 +105,21 @@ module Jobs
|
|||
ENV["DISCOURSE_LOG_SIDEKIQ"] == "1"
|
||||
end
|
||||
|
||||
def self.mutex
|
||||
@@mutex ||= Mutex.new
|
||||
end
|
||||
|
||||
def self.ensure_interval_logging!
|
||||
interval = ENV["DISCOURSE_LOG_SIDEKIQ_INTERVAL"]
|
||||
return if !interval
|
||||
interval = interval.to_i
|
||||
@@interval_thread ||= Thread.new do
|
||||
begin
|
||||
loop do
|
||||
sleep interval.to_i
|
||||
@@active_jobs.each { |j| j.write_to_log if j.current_duration > interval }
|
||||
sleep interval
|
||||
mutex.synchronize do
|
||||
@@active_jobs.each { |j| j.write_to_log if j.current_duration > interval }
|
||||
end
|
||||
end
|
||||
rescue Exception => e
|
||||
Discourse.warn_exception(e, message: "Sidekiq interval logging thread terminated unexpectedly")
|
||||
|
@ -183,7 +201,7 @@ module Jobs
|
|||
exception = {}
|
||||
|
||||
RailsMultisite::ConnectionManagement.with_connection(db) do
|
||||
job_instrumenter = JobInstrumenter.new(job_class: self.class, opts: opts, db: db)
|
||||
job_instrumenter = JobInstrumenter.new(job_class: self.class, opts: opts, db: db, jid: jid)
|
||||
begin
|
||||
I18n.locale = SiteSetting.default_locale || "en"
|
||||
I18n.ensure_all_loaded!
|
||||
|
|
|
@ -24,7 +24,7 @@ module Jobs
|
|||
BadgeGranter.grant(badge, user)
|
||||
|
||||
SystemMessage.new(user).create('new_user_of_the_month',
|
||||
month_year: Time.now.strftime("%B %Y"),
|
||||
month_year: I18n.l(Time.now, format: :no_day),
|
||||
url: "#{Discourse.base_url}/badges"
|
||||
)
|
||||
end
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
class GoogleUserInfo < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: google_user_infos
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# user_id :integer not null
|
||||
# google_user_id :string not null
|
||||
# first_name :string
|
||||
# last_name :string
|
||||
# email :string
|
||||
# gender :string
|
||||
# name :string
|
||||
# link :string
|
||||
# profile_link :string
|
||||
# picture :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_google_user_infos_on_google_user_id (google_user_id) UNIQUE
|
||||
# index_google_user_infos_on_user_id (user_id) UNIQUE
|
||||
#
|
|
@ -290,7 +290,7 @@ class Group < ActiveRecord::Base
|
|||
# way to have the membership in a table
|
||||
case name
|
||||
when :everyone
|
||||
group.visibility_level = Group.visibility_levels[:owners]
|
||||
group.visibility_level = Group.visibility_levels[:staff]
|
||||
group.save!
|
||||
return group
|
||||
when :moderators
|
||||
|
|
|
@ -226,8 +226,9 @@ class Invite < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def self.rescind_all_invites_from(user)
|
||||
Invite.where('invites.user_id IS NULL AND invites.email IS NOT NULL AND invited_by_id = ?', user.id).find_each do |invite|
|
||||
def self.rescind_all_expired_invites_from(user)
|
||||
Invite.where('invites.user_id IS NULL AND invites.email IS NOT NULL AND invited_by_id = ? AND invites.created_at < ?',
|
||||
user.id, SiteSetting.invite_expiry_days.days.ago).find_each do |invite|
|
||||
invite.trash!(user)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -407,8 +407,8 @@ class OptimizedImage < ActiveRecord::Base
|
|||
# just ditch the optimized image if there was any errors
|
||||
optimized_image.destroy
|
||||
ensure
|
||||
file&.unlink
|
||||
file&.close
|
||||
file&.unlink if file&.respond_to?(:unlink)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -194,6 +194,7 @@ class Post < ActiveRecord::Base
|
|||
def recover!
|
||||
super
|
||||
update_flagged_posts_count
|
||||
delete_post_notices
|
||||
recover_public_post_actions
|
||||
TopicLink.extract_from(self)
|
||||
QuotedPost.extract_from(self)
|
||||
|
@ -381,6 +382,11 @@ class Post < ActiveRecord::Base
|
|||
PostAction.update_flagged_posts_count
|
||||
end
|
||||
|
||||
def delete_post_notices
|
||||
self.custom_fields.delete("post_notice_type")
|
||||
self.custom_fields.delete("post_notice_time")
|
||||
end
|
||||
|
||||
def recover_public_post_actions
|
||||
PostAction.publics
|
||||
.with_deleted
|
||||
|
|
|
@ -17,15 +17,18 @@ class S3RegionSiteSetting < EnumSiteSetting
|
|||
'ap-south-1',
|
||||
'ap-southeast-1',
|
||||
'ap-southeast-2',
|
||||
'ca-central-1',
|
||||
'cn-north-1',
|
||||
'cn-northwest-1',
|
||||
'eu-central-1',
|
||||
'eu-north-1',
|
||||
'eu-west-1',
|
||||
'eu-west-2',
|
||||
'eu-west-3',
|
||||
'sa-east-1',
|
||||
'us-east-1',
|
||||
'us-east-2',
|
||||
'us-gov-east-1',
|
||||
'us-gov-west-1',
|
||||
'us-west-1',
|
||||
'us-west-2',
|
||||
|
|
|
@ -527,10 +527,10 @@ class Topic < ActiveRecord::Base
|
|||
end
|
||||
|
||||
# Atomically creates the next post number
|
||||
def self.next_post_number(topic_id, reply = false, whisper = false)
|
||||
def self.next_post_number(topic_id, opts = {})
|
||||
highest = DB.query_single("SELECT coalesce(max(post_number),0) AS max FROM posts WHERE topic_id = ?", topic_id).first.to_i
|
||||
|
||||
if whisper
|
||||
if opts[:whisper]
|
||||
|
||||
result = DB.query_single(<<~SQL, highest, topic_id)
|
||||
UPDATE topics
|
||||
|
@ -543,13 +543,15 @@ class Topic < ActiveRecord::Base
|
|||
|
||||
else
|
||||
|
||||
reply_sql = reply ? ", reply_count = reply_count + 1" : ""
|
||||
reply_sql = opts[:reply] ? ", reply_count = reply_count + 1" : ""
|
||||
posts_sql = opts[:post] ? ", posts_count = posts_count + 1" : ""
|
||||
|
||||
result = DB.query_single(<<~SQL, highest: highest, topic_id: topic_id)
|
||||
UPDATE topics
|
||||
SET highest_staff_post_number = :highest + 1,
|
||||
highest_post_number = :highest + 1#{reply_sql},
|
||||
posts_count = posts_count + 1
|
||||
highest_post_number = :highest + 1
|
||||
#{reply_sql}
|
||||
#{posts_sql}
|
||||
WHERE id = :topic_id
|
||||
RETURNING highest_post_number
|
||||
SQL
|
||||
|
@ -585,6 +587,43 @@ class Topic < ActiveRecord::Base
|
|||
posts_count = Y.posts_count
|
||||
FROM X, Y
|
||||
WHERE
|
||||
topics.archetype <> 'private_message' AND
|
||||
X.topic_id = topics.id AND
|
||||
Y.topic_id = topics.id AND (
|
||||
topics.highest_staff_post_number <> X.highest_post_number OR
|
||||
topics.highest_post_number <> Y.highest_post_number OR
|
||||
topics.last_posted_at <> Y.last_posted_at OR
|
||||
topics.posts_count <> Y.posts_count
|
||||
)
|
||||
SQL
|
||||
|
||||
DB.exec <<~SQL
|
||||
WITH
|
||||
X as (
|
||||
SELECT topic_id,
|
||||
COALESCE(MAX(post_number), 0) highest_post_number
|
||||
FROM posts
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY topic_id
|
||||
),
|
||||
Y as (
|
||||
SELECT topic_id,
|
||||
coalesce(MAX(post_number), 0) highest_post_number,
|
||||
count(*) posts_count,
|
||||
max(created_at) last_posted_at
|
||||
FROM posts
|
||||
WHERE deleted_at IS NULL AND post_type <> 3 AND post_type <> 4
|
||||
GROUP BY topic_id
|
||||
)
|
||||
UPDATE topics
|
||||
SET
|
||||
highest_staff_post_number = X.highest_post_number,
|
||||
highest_post_number = Y.highest_post_number,
|
||||
last_posted_at = Y.last_posted_at,
|
||||
posts_count = Y.posts_count
|
||||
FROM X, Y
|
||||
WHERE
|
||||
topics.archetype = 'private_message' AND
|
||||
X.topic_id = topics.id AND
|
||||
Y.topic_id = topics.id AND (
|
||||
topics.highest_staff_post_number <> X.highest_post_number OR
|
||||
|
@ -597,32 +636,39 @@ class Topic < ActiveRecord::Base
|
|||
|
||||
# If a post is deleted we have to update our highest post counters
|
||||
def self.reset_highest(topic_id)
|
||||
archetype = Topic.where(id: topic_id).pluck(:archetype).first
|
||||
|
||||
# ignore small_action replies for private messages
|
||||
post_type = archetype == Archetype.private_message ? " AND post_type <> #{Post.types[:small_action]}" : ''
|
||||
|
||||
result = DB.query_single(<<~SQL, topic_id: topic_id)
|
||||
UPDATE topics
|
||||
SET
|
||||
highest_staff_post_number = (
|
||||
highest_staff_post_number = (
|
||||
SELECT COALESCE(MAX(post_number), 0) FROM posts
|
||||
WHERE topic_id = :topic_id AND
|
||||
deleted_at IS NULL
|
||||
),
|
||||
highest_post_number = (
|
||||
highest_post_number = (
|
||||
SELECT COALESCE(MAX(post_number), 0) FROM posts
|
||||
WHERE topic_id = :topic_id AND
|
||||
deleted_at IS NULL AND
|
||||
post_type <> 4
|
||||
#{post_type}
|
||||
),
|
||||
posts_count = (
|
||||
SELECT count(*) FROM posts
|
||||
WHERE deleted_at IS NULL AND
|
||||
topic_id = :topic_id AND
|
||||
post_type <> 4
|
||||
#{post_type}
|
||||
),
|
||||
|
||||
last_posted_at = (
|
||||
SELECT MAX(created_at) FROM posts
|
||||
WHERE topic_id = :topic_id AND
|
||||
deleted_at IS NULL AND
|
||||
post_type <> 4
|
||||
#{post_type}
|
||||
)
|
||||
WHERE id = :topic_id
|
||||
RETURNING highest_post_number
|
||||
|
|
|
@ -66,7 +66,6 @@ class User < ActiveRecord::Base
|
|||
has_one :user_avatar, dependent: :destroy
|
||||
has_many :user_associated_accounts, dependent: :destroy
|
||||
has_one :github_user_info, dependent: :destroy
|
||||
has_one :google_user_info, dependent: :destroy
|
||||
has_many :oauth2_user_infos, dependent: :destroy
|
||||
has_one :instagram_user_info, dependent: :destroy
|
||||
has_many :user_second_factors, dependent: :destroy
|
||||
|
|
|
@ -65,10 +65,12 @@ class WebHook < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def self.enqueue_topic_hooks(event, topic)
|
||||
def self.enqueue_topic_hooks(event, topic, payload = nil)
|
||||
if active_web_hooks('topic').exists? && topic.present?
|
||||
topic_view = TopicView.new(topic.id, Discourse.system_user)
|
||||
payload = WebHook.generate_payload(:topic, topic_view, WebHookTopicViewSerializer)
|
||||
payload ||= begin
|
||||
topic_view = TopicView.new(topic.id, Discourse.system_user)
|
||||
WebHook.generate_payload(:topic, topic_view, WebHookTopicViewSerializer)
|
||||
end
|
||||
|
||||
WebHook.enqueue_hooks(:topic, event,
|
||||
id: topic.id,
|
||||
|
@ -79,9 +81,9 @@ class WebHook < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def self.enqueue_post_hooks(event, post)
|
||||
def self.enqueue_post_hooks(event, post, payload = nil)
|
||||
if active_web_hooks('post').exists? && post.present?
|
||||
payload = WebHook.generate_payload(:post, post)
|
||||
payload ||= WebHook.generate_payload(:post, post)
|
||||
|
||||
WebHook.enqueue_hooks(:post, event,
|
||||
id: post.id,
|
||||
|
|
|
@ -6,7 +6,8 @@ class BasicPostSerializer < ApplicationSerializer
|
|||
:avatar_template,
|
||||
:created_at,
|
||||
:cooked,
|
||||
:cooked_hidden
|
||||
:cooked_hidden,
|
||||
:ignored
|
||||
|
||||
def name
|
||||
object.user && object.user.name
|
||||
|
@ -35,11 +36,18 @@ class BasicPostSerializer < ApplicationSerializer
|
|||
else
|
||||
I18n.t('flagging.user_must_edit')
|
||||
end
|
||||
elsif ignored
|
||||
I18n.t('ignored.hidden_content')
|
||||
else
|
||||
object.filter_quotes(@parent_post)
|
||||
end
|
||||
end
|
||||
|
||||
def ignored
|
||||
object.is_first_post? && IgnoredUser.where(user_id: scope.current_user&.id,
|
||||
ignored_user_id: object.user_id).present?
|
||||
end
|
||||
|
||||
def include_name?
|
||||
SiteSetting.enable_names?
|
||||
end
|
||||
|
|
|
@ -70,6 +70,8 @@ class PostSerializer < BasicPostSerializer
|
|||
:is_auto_generated,
|
||||
:action_code,
|
||||
:action_code_who,
|
||||
:post_notice_type,
|
||||
:post_notice_time,
|
||||
:last_wiki_edit,
|
||||
:locked,
|
||||
:excerpt
|
||||
|
@ -363,6 +365,22 @@ class PostSerializer < BasicPostSerializer
|
|||
include_action_code? && action_code_who.present?
|
||||
end
|
||||
|
||||
def post_notice_type
|
||||
post_custom_fields["post_notice_type"]
|
||||
end
|
||||
|
||||
def include_post_notice_type?
|
||||
post_notice_type.present?
|
||||
end
|
||||
|
||||
def post_notice_time
|
||||
post_custom_fields["post_notice_time"]
|
||||
end
|
||||
|
||||
def include_post_notice_time?
|
||||
post_notice_time.present?
|
||||
end
|
||||
|
||||
def locked
|
||||
true
|
||||
end
|
||||
|
|
|
@ -53,7 +53,6 @@ class UserAnonymizer
|
|||
end
|
||||
|
||||
@user.user_avatar.try(:destroy)
|
||||
@user.google_user_info.try(:destroy)
|
||||
@user.github_user_info.try(:destroy)
|
||||
@user.single_sign_on_record.try(:destroy)
|
||||
@user.oauth2_user_infos.try(:destroy_all)
|
||||
|
|
|
@ -128,6 +128,10 @@ class UserUpdater
|
|||
update_muted_users(attributes[:muted_usernames])
|
||||
end
|
||||
|
||||
if attributes.key?(:ignored_usernames)
|
||||
update_ignored_users(attributes[:ignored_usernames])
|
||||
end
|
||||
|
||||
name_changed = user.name_changed?
|
||||
if (saved = (!save_options || user.user_option.save) && user_profile.save && user.save) &&
|
||||
(name_changed && old_user_name.casecmp(attributes.fetch(:name)) != 0)
|
||||
|
@ -157,13 +161,27 @@ class UserUpdater
|
|||
INSERT into muted_users(user_id, muted_user_id, created_at, updated_at)
|
||||
SELECT :user_id, id, :now, :now
|
||||
FROM users
|
||||
WHERE
|
||||
id in (:desired_ids) AND
|
||||
id NOT IN (
|
||||
SELECT muted_user_id
|
||||
FROM muted_users
|
||||
WHERE user_id = :user_id
|
||||
)
|
||||
WHERE id in (:desired_ids)
|
||||
ON CONFLICT DO NOTHING
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def update_ignored_users(usernames)
|
||||
usernames ||= ""
|
||||
desired_ids = User.where(username: usernames.split(",")).pluck(:id)
|
||||
if desired_ids.empty?
|
||||
IgnoredUser.where(user_id: user.id).destroy_all
|
||||
else
|
||||
IgnoredUser.where('user_id = ? AND ignored_user_id not in (?)', user.id, desired_ids).destroy_all
|
||||
|
||||
# SQL is easier here than figuring out how to do the same in AR
|
||||
DB.exec(<<~SQL, now: Time.now, user_id: user.id, desired_ids: desired_ids)
|
||||
INSERT into ignored_users(user_id, ignored_user_id, created_at, updated_at)
|
||||
SELECT :user_id, id, :now, :now
|
||||
FROM users
|
||||
WHERE id in (:desired_ids)
|
||||
ON CONFLICT DO NOTHING
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
<meta name="shared_session_key" content="<%= shared_session_key %>">
|
||||
<%- end %>
|
||||
|
||||
<%= build_plugin_html 'server:before-script-load' %>
|
||||
|
||||
<%= preload_script "locales/#{I18n.locale}" %>
|
||||
<%= preload_script "ember_jquery" %>
|
||||
<%= preload_script "preload-store" %>
|
||||
|
|
|
@ -192,15 +192,18 @@ en:
|
|||
ap_south_1: "Asia Pacific (Mumbai)"
|
||||
ap_southeast_1: "Asia Pacific (Singapore)"
|
||||
ap_southeast_2: "Asia Pacific (Sydney)"
|
||||
ca_central_1: "Canada (Central)"
|
||||
cn_north_1: "China (Beijing)"
|
||||
cn_northwest_1: "China (Ningxia)"
|
||||
eu_central_1: "EU (Frankfurt)"
|
||||
eu_north_1: "EU (Stockholm)"
|
||||
eu_west_1: "EU (Ireland)"
|
||||
eu_west_2: "EU (London)"
|
||||
eu_west_3: "EU (Paris)"
|
||||
sa_east_1: "South America (Sao Paulo)"
|
||||
sa_east_1: "South America (São Paulo)"
|
||||
us_east_1: "US East (N. Virginia)"
|
||||
us_east_2: "US East (Ohio)"
|
||||
us_gov_east_1: "AWS GovCloud (US-East)"
|
||||
us_gov_west_1: "AWS GovCloud (US)"
|
||||
us_west_1: "US West (N. California)"
|
||||
us_west_2: "US West (Oregon)"
|
||||
|
@ -1000,9 +1003,9 @@ en:
|
|||
expired: "This invite has expired."
|
||||
rescind: "Remove"
|
||||
rescinded: "Invite removed"
|
||||
rescind_all: "Remove all Invites"
|
||||
rescinded_all: "All Invites removed!"
|
||||
rescind_all_confirm: "Are you sure you want to remove all invites?"
|
||||
rescind_all: "Remove all Expired Invites"
|
||||
rescinded_all: "All Expired Invites removed!"
|
||||
rescind_all_confirm: "Are you sure you want to remove all expired invites?"
|
||||
reinvite: "Resend Invite"
|
||||
reinvite_all: "Resend all Invites"
|
||||
reinvite_all_confirm: "Are you sure you want to resend all invites?"
|
||||
|
@ -1809,6 +1812,7 @@ en:
|
|||
when: "When:"
|
||||
public_timer_types: Topic Timers
|
||||
private_timer_types: User Topic Timers
|
||||
time_frame_required: Please select a time frame
|
||||
auto_update_input:
|
||||
none: "Select a timeframe"
|
||||
later_today: "Later today"
|
||||
|
@ -2145,6 +2149,10 @@ en:
|
|||
one: "view 1 hidden reply"
|
||||
other: "view {{count}} hidden replies"
|
||||
|
||||
notice:
|
||||
first: "This is the first time {{user}} has posted — let's welcome them to our community!"
|
||||
return: "It's been a while since we've seen {{user}} — their last post was in {{time}}."
|
||||
|
||||
unread: "Post is unread"
|
||||
has_replies:
|
||||
one: "{{count}} Reply"
|
||||
|
|
|
@ -25,14 +25,16 @@ en:
|
|||
|
||||
datetime_formats: &datetime_formats
|
||||
formats:
|
||||
# Format directives: https://ruby-doc.org/core-2.3.1/Time.html#method-i-strftime
|
||||
# Format directives: https://ruby-doc.org/core-2.6.1/Time.html#method-i-strftime
|
||||
short: "%m-%d-%Y"
|
||||
# Format directives: https://ruby-doc.org/core-2.3.1/Time.html#method-i-strftime
|
||||
# Format directives: https://ruby-doc.org/core-2.6.1/Time.html#method-i-strftime
|
||||
short_no_year: "%B %-d"
|
||||
# Format directives: https://ruby-doc.org/core-2.3.1/Time.html#method-i-strftime
|
||||
# Format directives: https://ruby-doc.org/core-2.6.1/Time.html#method-i-strftime
|
||||
date_only: "%B %-d, %Y"
|
||||
# Format directives: https://ruby-doc.org/core-2.3.1/Time.html#method-i-strftime
|
||||
# Format directives: https://ruby-doc.org/core-2.6.1/Time.html#method-i-strftime
|
||||
long: "%B %-d, %Y, %l:%M%P"
|
||||
# Format directives: https://ruby-doc.org/core-2.6.1/Time.html#method-i-strftime
|
||||
no_day: "%B %Y"
|
||||
date:
|
||||
# Do not remove the brackets and commas and do not translate the first month name. It should be "null".
|
||||
month_names:
|
||||
|
@ -870,6 +872,9 @@ en:
|
|||
you_must_edit: '<p>Your post was flagged by the community. Please <a href="%{path}">see your messages</a>.</p>'
|
||||
user_must_edit: "<p>This post was flagged by the community and is temporarily hidden.</p>"
|
||||
|
||||
ignored:
|
||||
hidden_content: '<p>Hidden content</p>'
|
||||
|
||||
archetypes:
|
||||
regular:
|
||||
title: "Regular Topic"
|
||||
|
@ -1896,6 +1901,8 @@ en:
|
|||
max_allowed_message_recipients: "Maximum recipients allowed in a message."
|
||||
watched_words_regular_expressions: "Watched words are regular expressions."
|
||||
|
||||
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."
|
||||
default_include_tl0_in_digests: "Include posts from new users in summary emails by default. Users can change this in their preferences."
|
||||
default_email_personal_messages: "Send an email when someone messages the user by default."
|
||||
|
|
|
@ -807,6 +807,8 @@ posting:
|
|||
default: false
|
||||
client: true
|
||||
shadowed_by_global: true
|
||||
returning_users_days:
|
||||
default: 60
|
||||
|
||||
email:
|
||||
email_time_window_mins:
|
||||
|
@ -1251,7 +1253,7 @@ security:
|
|||
list_type: compact
|
||||
blacklisted_crawler_user_agents:
|
||||
type: list
|
||||
default: "mauibot|semrushbot|ahrefsbot"
|
||||
default: "mauibot|semrushbot|ahrefsbot|blexbot"
|
||||
list_type: compact
|
||||
slow_down_crawler_user_agents:
|
||||
type: list
|
||||
|
|
27
db/migrate/20190306154335_migrate_google_user_info.rb
Normal file
27
db/migrate/20190306154335_migrate_google_user_info.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
class MigrateGoogleUserInfo < ActiveRecord::Migration[5.2]
|
||||
def up
|
||||
execute <<~SQL
|
||||
INSERT INTO user_associated_accounts (
|
||||
provider_name,
|
||||
provider_uid,
|
||||
user_id,
|
||||
info,
|
||||
last_used,
|
||||
created_at,
|
||||
updated_at
|
||||
) SELECT
|
||||
'google_oauth2',
|
||||
google_user_id,
|
||||
user_id,
|
||||
json_build_object('email', email, 'first_name', first_name, 'last_name', last_name, 'name', name),
|
||||
updated_at,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM google_user_infos
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
|
@ -1,5 +1,4 @@
|
|||
class Auth::GoogleOAuth2Authenticator < Auth::Authenticator
|
||||
|
||||
class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator
|
||||
def name
|
||||
"google_oauth2"
|
||||
end
|
||||
|
@ -8,77 +7,10 @@ class Auth::GoogleOAuth2Authenticator < Auth::Authenticator
|
|||
SiteSetting.enable_google_oauth2_logins
|
||||
end
|
||||
|
||||
def description_for_user(user)
|
||||
info = GoogleUserInfo.find_by(user_id: user.id)
|
||||
info&.email || info&.name || ""
|
||||
end
|
||||
|
||||
def can_revoke?
|
||||
true
|
||||
end
|
||||
|
||||
def revoke(user, skip_remote: false)
|
||||
info = GoogleUserInfo.find_by(user_id: user.id)
|
||||
raise Discourse::NotFound if info.nil?
|
||||
|
||||
# We get a temporary token from google upon login but do not need it, and do not store it.
|
||||
# Therefore we do not have any way to revoke the token automatically on google's end
|
||||
|
||||
info.destroy!
|
||||
true
|
||||
end
|
||||
|
||||
def can_connect_existing_user?
|
||||
true
|
||||
end
|
||||
|
||||
def after_authenticate(auth_hash, existing_account: nil)
|
||||
session_info = parse_hash(auth_hash)
|
||||
google_hash = session_info[:google]
|
||||
|
||||
result = ::Auth::Result.new
|
||||
result.email = session_info[:email]
|
||||
result.email_valid = session_info[:email_valid]
|
||||
result.name = session_info[:name]
|
||||
|
||||
result.extra_data = google_hash
|
||||
|
||||
user_info = ::GoogleUserInfo.find_by(google_user_id: google_hash[:google_user_id])
|
||||
|
||||
if existing_account && (user_info.nil? || existing_account.id != user_info.user_id)
|
||||
user_info.destroy! if user_info
|
||||
result.user = existing_account
|
||||
user_info = GoogleUserInfo.create!({ user_id: result.user.id }.merge(google_hash))
|
||||
else
|
||||
result.user = user_info&.user
|
||||
end
|
||||
|
||||
if !result.user && !result.email.blank? && result.email_valid
|
||||
result.user = User.find_by_email(result.email)
|
||||
if result.user
|
||||
# we've matched an existing user to this login attempt...
|
||||
if result.user.google_user_info && result.user.google_user_info.google_user_id != google_hash[:google_user_id]
|
||||
# but the user has changed the google account used to log in...
|
||||
if result.user.google_user_info.email != google_hash[:email]
|
||||
# the user changed their email, go ahead and scrub the old record
|
||||
result.user.google_user_info.destroy!
|
||||
else
|
||||
# same email address but different account? likely a takeover scenario
|
||||
result.failed = true
|
||||
result.failed_reason = I18n.t('errors.conflicting_google_user_id')
|
||||
return result
|
||||
end
|
||||
end
|
||||
::GoogleUserInfo.create({ user_id: result.user.id }.merge(google_hash))
|
||||
end
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def after_create_account(user, auth)
|
||||
data = auth[:extra_data]
|
||||
GoogleUserInfo.create({ user_id: user.id }.merge(data))
|
||||
def primary_email_verified?(auth_token)
|
||||
# note, emails that come back from google via omniauth are always valid
|
||||
# this protects against future regressions
|
||||
auth_token[:extra][:raw_info][:email_verified]
|
||||
end
|
||||
|
||||
def register_middleware(omniauth)
|
||||
|
@ -95,37 +27,8 @@ class Auth::GoogleOAuth2Authenticator < Auth::Authenticator
|
|||
if (google_oauth2_prompt = SiteSetting.google_oauth2_prompt).present?
|
||||
strategy.options[:prompt] = google_oauth2_prompt.gsub("|", " ")
|
||||
end
|
||||
},
|
||||
skip_jwt: true
|
||||
}
|
||||
}
|
||||
# jwt encoding is causing auth to fail in quite a few conditions
|
||||
# skipping
|
||||
omniauth.provider :google_oauth2, options
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def parse_hash(hash)
|
||||
extra = hash[:extra][:raw_info]
|
||||
|
||||
h = {}
|
||||
|
||||
h[:email] = hash[:info][:email]
|
||||
h[:name] = hash[:info][:name]
|
||||
h[:email_valid] = extra[:email_verified]
|
||||
|
||||
h[:google] = {
|
||||
google_user_id: hash[:uid] || extra[:sub],
|
||||
email: extra[:email],
|
||||
first_name: extra[:given_name],
|
||||
last_name: extra[:family_name],
|
||||
gender: extra[:gender],
|
||||
name: extra[:name],
|
||||
link: extra[:hd],
|
||||
profile_link: extra[:profile],
|
||||
picture: extra[:picture]
|
||||
}
|
||||
|
||||
h
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,12 @@ class Auth::ManagedAuthenticator < Auth::Authenticator
|
|||
true
|
||||
end
|
||||
|
||||
def primary_email_verified?(auth_token)
|
||||
# Omniauth providers should only provide verified emails in the :info hash.
|
||||
# This method allows additional checks to be added
|
||||
true
|
||||
end
|
||||
|
||||
def can_revoke?
|
||||
true
|
||||
end
|
||||
|
@ -35,7 +41,11 @@ class Auth::ManagedAuthenticator < Auth::Authenticator
|
|||
end
|
||||
|
||||
# Matching an account by email
|
||||
if match_by_email && association.user.nil? && (user = User.find_by_email(auth_token.dig(:info, :email)))
|
||||
if primary_email_verified?(auth_token) &&
|
||||
match_by_email &&
|
||||
association.user.nil? &&
|
||||
(user = User.find_by_email(auth_token.dig(:info, :email)))
|
||||
|
||||
UserAssociatedAccount.where(user: user, provider_name: auth_token[:provider]).destroy_all # Destroy existing associations for the new user
|
||||
association.user = user
|
||||
end
|
||||
|
@ -60,7 +70,7 @@ class Auth::ManagedAuthenticator < Auth::Authenticator
|
|||
result.email = info[:email]
|
||||
result.name = "#{info[:first_name]} #{info[:last_name]}"
|
||||
result.username = info[:nickname]
|
||||
result.email_valid = true if result.email
|
||||
result.email_valid = primary_email_verified?(auth_token) if result.email
|
||||
result.extra_data = {
|
||||
provider: auth_token[:provider],
|
||||
uid: auth_token[:uid]
|
||||
|
|
|
@ -29,6 +29,7 @@ module BackupRestore
|
|||
@client_id = opts[:client_id]
|
||||
@filename = opts[:filename]
|
||||
@publish_to_message_bus = opts[:publish_to_message_bus] || false
|
||||
@disable_emails = opts.fetch(:disable_emails, true)
|
||||
|
||||
ensure_restore_is_enabled
|
||||
ensure_no_operation_is_running
|
||||
|
@ -402,9 +403,11 @@ module BackupRestore
|
|||
log "Reloading site settings..."
|
||||
SiteSetting.refresh!
|
||||
|
||||
log "Disabling outgoing emails for non-staff users..."
|
||||
user = User.find_by_email(@user_info[:email]) || Discourse.system_user
|
||||
SiteSetting.set_and_log(:disable_emails, 'non-staff', user)
|
||||
if @disable_emails
|
||||
log "Disabling outgoing emails for non-staff users..."
|
||||
user = User.find_by_email(@user_info[:email]) || Discourse.system_user
|
||||
SiteSetting.set_and_log(:disable_emails, 'non-staff', user)
|
||||
end
|
||||
end
|
||||
|
||||
def clear_emoji_cache
|
||||
|
|
|
@ -899,6 +899,22 @@ module Email
|
|||
create_post_with_attachments(options)
|
||||
end
|
||||
|
||||
def notification_level_for(body)
|
||||
# since we are stripping save all this work on long replies
|
||||
return nil if body.length > 40
|
||||
|
||||
body = body.strip.downcase
|
||||
case body
|
||||
when "mute"
|
||||
NotificationLevels.topic_levels[:muted]
|
||||
when "track"
|
||||
NotificationLevels.topic_levels[:tracking]
|
||||
when "watch"
|
||||
NotificationLevels.topic_levels[:watching]
|
||||
else nil
|
||||
end
|
||||
end
|
||||
|
||||
def create_reply(options = {})
|
||||
raise TopicNotFoundError if options[:topic].nil? || options[:topic].trashed?
|
||||
raise BouncedEmailError if options[:bounce] && options[:topic].archetype != Archetype.private_message
|
||||
|
@ -908,6 +924,8 @@ module Email
|
|||
|
||||
if post_action_type = post_action_for(options[:raw])
|
||||
create_post_action(options[:user], options[:post], post_action_type)
|
||||
elsif notification_level = notification_level_for(options[:raw])
|
||||
TopicUser.change(options[:user].id, options[:post].topic_id, notification_level: notification_level)
|
||||
else
|
||||
raise TopicClosedError if options[:topic].closed?
|
||||
options[:topic_id] = options[:topic].id
|
||||
|
|
|
@ -86,7 +86,7 @@ module JsLocaleHelper
|
|||
end
|
||||
|
||||
def self.load_translations_merged(*locales)
|
||||
locales = locales.compact
|
||||
locales = locales.uniq.compact
|
||||
@loaded_merges ||= {}
|
||||
@loaded_merges[locales.join('-')] ||= begin
|
||||
all_translations = {}
|
||||
|
|
|
@ -165,6 +165,7 @@ class PostCreator
|
|||
transaction do
|
||||
build_post_stats
|
||||
create_topic
|
||||
create_post_notice
|
||||
save_post
|
||||
extract_links
|
||||
track_topic
|
||||
|
@ -247,7 +248,11 @@ class PostCreator
|
|||
post.word_count = post.raw.scan(/[[:word:]]+/).size
|
||||
|
||||
whisper = post.post_type == Post.types[:whisper]
|
||||
post.post_number ||= Topic.next_post_number(post.topic_id, post.reply_to_post_number.present?, whisper)
|
||||
increase_posts_count = !post.topic&.private_message? || post.post_type != Post.types[:small_action]
|
||||
post.post_number ||= Topic.next_post_number(post.topic_id,
|
||||
reply: post.reply_to_post_number.present?,
|
||||
whisper: whisper,
|
||||
post: increase_posts_count)
|
||||
|
||||
cooking_options = post.cooking_options || {}
|
||||
cooking_options[:topic_id] = post.topic_id
|
||||
|
@ -508,6 +513,21 @@ class PostCreator
|
|||
@user.update_attributes(last_posted_at: @post.created_at)
|
||||
end
|
||||
|
||||
def create_post_notice
|
||||
last_post_time = Post.where(user_id: @user.id)
|
||||
.order(created_at: :desc)
|
||||
.limit(1)
|
||||
.pluck(:created_at)
|
||||
.first
|
||||
|
||||
if !last_post_time
|
||||
@post.custom_fields["post_notice_type"] = "first"
|
||||
elsif SiteSetting.returning_users_days > 0 && last_post_time < SiteSetting.returning_users_days.days.ago
|
||||
@post.custom_fields["post_notice_type"] = "returning"
|
||||
@post.custom_fields["post_notice_time"] = last_post_time.iso8601
|
||||
end
|
||||
end
|
||||
|
||||
def publish
|
||||
return if @opts[:import_mode] || @post.post_number == 1
|
||||
@post.publish_change_to_clients! :created
|
||||
|
|
|
@ -61,19 +61,11 @@ class PostDestroyer
|
|||
mark_for_deletion(delete_removed_posts_after)
|
||||
end
|
||||
DiscourseEvent.trigger(:post_destroyed, @post, @opts, @user)
|
||||
WebHook.enqueue_hooks(:post, :post_destroyed,
|
||||
id: @post.id,
|
||||
category_id: @post&.topic&.category_id,
|
||||
payload: payload
|
||||
) if WebHook.active_web_hooks(:post).exists?
|
||||
WebHook.enqueue_post_hooks(:post_destroyed, @post, payload)
|
||||
|
||||
if @post.is_first_post? && @post.topic
|
||||
DiscourseEvent.trigger(:topic_destroyed, @post.topic, @user)
|
||||
WebHook.enqueue_hooks(:topic, :topic_destroyed,
|
||||
id: topic.id,
|
||||
category_id: topic&.category_id,
|
||||
payload: topic_payload
|
||||
) if WebHook.active_web_hooks(:topic).exists?
|
||||
WebHook.enqueue_topic_hooks(:topic_destroyed, @post.topic, topic_payload)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -147,7 +139,7 @@ class PostDestroyer
|
|||
update_user_counts
|
||||
TopicUser.update_post_action_cache(post_id: @post.id)
|
||||
DB.after_commit do
|
||||
if @opts[:defer_flags].to_s == "true"
|
||||
if @opts[:defer_flags]
|
||||
defer_flags
|
||||
else
|
||||
agree_with_flags
|
||||
|
|
|
@ -230,10 +230,11 @@ module PrettyText
|
|||
return title unless SiteSetting.enable_emoji?
|
||||
|
||||
set = SiteSetting.emoji_set.inspect
|
||||
custom = Emoji.custom.map { |e| [e.name, e.url] }.to_h.to_json
|
||||
protect do
|
||||
v8.eval(<<~JS)
|
||||
__paths = #{paths_json};
|
||||
__performEmojiUnescape(#{title.inspect}, { getURL: __getURL, emojiSet: #{set} });
|
||||
__performEmojiUnescape(#{title.inspect}, { getURL: __getURL, emojiSet: #{set}, customEmoji: #{custom} });
|
||||
JS
|
||||
end
|
||||
end
|
||||
|
|
|
@ -687,7 +687,7 @@ class Search
|
|||
def groups_search
|
||||
groups = Group
|
||||
.visible_groups(@guardian.user, "name ASC", include_everyone: false)
|
||||
.where("groups.name ILIKE ?", "%#{@term}%")
|
||||
.where("name ILIKE :term OR full_name ILIKE :term", term: "%#{@term}%")
|
||||
|
||||
groups.each { |group| @results.add(group) }
|
||||
end
|
||||
|
|
|
@ -144,9 +144,8 @@ COMMENT
|
|||
end
|
||||
|
||||
def to_scss_variable(name, value)
|
||||
escaped = value.to_s.gsub('"', "\\22")
|
||||
escaped.gsub!("\n", "\\A")
|
||||
"$#{name}: unquote(\"#{escaped}\");\n"
|
||||
escaped = SassC::Script::Value::String.quote(value, sass: true)
|
||||
"$#{name}: unquote(#{escaped});\n"
|
||||
end
|
||||
|
||||
def imports(asset, parent_path)
|
||||
|
|
|
@ -118,6 +118,7 @@ module SvgSprite
|
|||
"globe",
|
||||
"globe-americas",
|
||||
"hand-point-right",
|
||||
"hands-helping",
|
||||
"heading",
|
||||
"heart",
|
||||
"home",
|
||||
|
@ -224,10 +225,10 @@ module SvgSprite
|
|||
icons = all_icons(theme_ids)
|
||||
|
||||
doc = File.open("#{Rails.root}/vendor/assets/svg-icons/fontawesome/solid.svg") { |f| Nokogiri::XML(f) }
|
||||
fa_license = doc.at('//comment()').text
|
||||
|
||||
svg_subset = """<!--
|
||||
Discourse SVG subset of #{fa_license}
|
||||
Discourse SVG subset of Font Awesome Free by @fontawesome - https://fontawesome.com
|
||||
License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
-->
|
||||
<svg xmlns='http://www.w3.org/2000/svg' style='display: none;'>
|
||||
""".dup
|
||||
|
|
|
@ -58,7 +58,7 @@ task 'assets:precompile:css' => 'environment' do
|
|||
STDERR.puts "Compiling css for #{db} #{Time.zone.now}"
|
||||
begin
|
||||
Stylesheet::Manager.precompile_css
|
||||
rescue PG::UndefinedColumn => e
|
||||
rescue PG::UndefinedColumn, ActiveModel::MissingAttributeError => e
|
||||
STDERR.puts "#{e.class} #{e.message}: #{e.backtrace.join("\n")}"
|
||||
STDERR.puts "Skipping precompilation of CSS cause schema is old, you are precompiling prior to running migrations."
|
||||
end
|
||||
|
|
|
@ -495,102 +495,6 @@ def list_missing_uploads(skip_optimized: false)
|
|||
Discourse.store.list_missing_uploads(skip_optimized: skip_optimized)
|
||||
end
|
||||
|
||||
################################################################################
|
||||
# Recover from tombstone #
|
||||
################################################################################
|
||||
|
||||
task "uploads:recover_from_tombstone" => :environment do
|
||||
if ENV["RAILS_DB"]
|
||||
recover_from_tombstone
|
||||
else
|
||||
RailsMultisite::ConnectionManagement.each_connection { recover_from_tombstone }
|
||||
end
|
||||
end
|
||||
|
||||
def recover_from_tombstone
|
||||
if Discourse.store.external?
|
||||
puts "This task only works for internal storages."
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
previous_image_size = SiteSetting.max_image_size_kb
|
||||
previous_attachment_size = SiteSetting.max_attachment_size_kb
|
||||
previous_extensions = SiteSetting.authorized_extensions
|
||||
|
||||
SiteSetting.max_image_size_kb = 10 * 1024
|
||||
SiteSetting.max_attachment_size_kb = 10 * 1024
|
||||
SiteSetting.authorized_extensions = "*"
|
||||
|
||||
current_db = RailsMultisite::ConnectionManagement.current_db
|
||||
public_path = Rails.root.join("public")
|
||||
paths = Dir.glob(File.join(public_path, 'uploads', 'tombstone', current_db, '**', '*.*'))
|
||||
max = paths.size
|
||||
|
||||
paths.each_with_index do |path, index|
|
||||
filename = File.basename(path)
|
||||
printf("%9d / %d (%5.1f%%)\n", (index + 1), max, (((index + 1).to_f / max.to_f) * 100).round(1))
|
||||
|
||||
Post.where("raw LIKE ?", "%#{filename}%").find_each do |post|
|
||||
doc = Nokogiri::HTML::fragment(post.raw)
|
||||
updated = false
|
||||
|
||||
image_urls = doc.css("img[src]").map { |img| img["src"] }
|
||||
attachment_urls = doc.css("a.attachment[href]").map { |a| a["href"] }
|
||||
|
||||
(image_urls + attachment_urls).each do |url|
|
||||
next if !url.start_with?("/uploads/")
|
||||
next if Upload.exists?(url: url)
|
||||
|
||||
puts "Restoring #{path}..."
|
||||
tombstone_path = File.join(public_path, 'uploads', 'tombstone', url.gsub(/^\/uploads\//, ""))
|
||||
|
||||
if File.exists?(tombstone_path)
|
||||
File.open(tombstone_path) do |file|
|
||||
new_upload = UploadCreator.new(file, File.basename(url)).create_for(Discourse::SYSTEM_USER_ID)
|
||||
|
||||
if new_upload.persisted?
|
||||
puts "Restored into #{new_upload.url}"
|
||||
DbHelper.remap(url, new_upload.url)
|
||||
updated = true
|
||||
else
|
||||
puts "Failed to create upload for #{url}: #{new_upload.errors.full_messages}."
|
||||
end
|
||||
end
|
||||
else
|
||||
puts "Failed to find file (#{tombstone_path}) in tombstone."
|
||||
end
|
||||
end
|
||||
|
||||
post.rebake! if updated
|
||||
end
|
||||
|
||||
sha1 = File.basename(filename, File.extname(filename))
|
||||
short_url = "upload://#{Base62.encode(sha1.hex)}"
|
||||
|
||||
Post.where("raw LIKE ?", "%#{short_url}%").find_each do |post|
|
||||
puts "Restoring #{path}..."
|
||||
|
||||
File.open(path) do |file|
|
||||
new_upload = UploadCreator.new(file, filename).create_for(Discourse::SYSTEM_USER_ID)
|
||||
|
||||
if new_upload.persisted?
|
||||
puts "Restored into #{new_upload.short_url}"
|
||||
DbHelper.remap(short_url, new_upload.short_url) if short_url != new_upload.short_url
|
||||
post.rebake!
|
||||
else
|
||||
puts "Failed to create upload for #{filename}: #{new_upload.errors.full_messages}."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
ensure
|
||||
SiteSetting.max_image_size_kb = previous_image_size
|
||||
SiteSetting.max_attachment_size_kb = previous_attachment_size
|
||||
SiteSetting.authorized_extensions = previous_extensions
|
||||
end
|
||||
end
|
||||
|
||||
################################################################################
|
||||
# regenerate_missing_optimized #
|
||||
################################################################################
|
||||
|
@ -795,6 +699,10 @@ task "uploads:fix_incorrect_extensions" => :environment do
|
|||
UploadFixer.fix_all_extensions
|
||||
end
|
||||
|
||||
task "uploads:recover_from_tombstone" => :environment do
|
||||
Rake::Task["uploads:recover"].invoke
|
||||
end
|
||||
|
||||
task "uploads:recover" => :environment do
|
||||
require_dependency "upload_recovery"
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ class TopicView
|
|||
end
|
||||
|
||||
def self.default_post_custom_fields
|
||||
@default_post_custom_fields ||= ["action_code_who"]
|
||||
@default_post_custom_fields ||= ["action_code_who", "post_notice_type", "post_notice_time"]
|
||||
end
|
||||
|
||||
def self.post_custom_fields_whitelisters
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"author": "Discourse",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "5.5.0",
|
||||
"@fortawesome/fontawesome-free": "5.7.2",
|
||||
"ace-builds": "1.4.2",
|
||||
"bootbox": "3.2.0",
|
||||
"chart.js": "2.7.3",
|
||||
|
|
|
@ -420,7 +420,9 @@ createWidget("discourse-poll-buttons", {
|
|||
const castVotesDisabled = !attrs.canCastVotes;
|
||||
contents.push(
|
||||
this.attach("button", {
|
||||
className: `btn cast-votes ${castVotesDisabled ? "" : "btn-primary"}`,
|
||||
className: `btn cast-votes ${
|
||||
castVotesDisabled ? "btn-default" : "btn-primary"
|
||||
}`,
|
||||
label: "poll.cast-votes.label",
|
||||
title: "poll.cast-votes.title",
|
||||
disabled: castVotesDisabled,
|
||||
|
@ -433,7 +435,7 @@ createWidget("discourse-poll-buttons", {
|
|||
if (attrs.showResults || hideResultsDisabled) {
|
||||
contents.push(
|
||||
this.attach("button", {
|
||||
className: "btn toggle-results",
|
||||
className: "btn btn-default toggle-results",
|
||||
label: "poll.hide-results.label",
|
||||
title: "poll.hide-results.title",
|
||||
icon: "far-eye-slash",
|
||||
|
@ -449,7 +451,7 @@ createWidget("discourse-poll-buttons", {
|
|||
} else {
|
||||
contents.push(
|
||||
this.attach("button", {
|
||||
className: "btn toggle-results",
|
||||
className: "btn btn-default toggle-results",
|
||||
label: "poll.show-results.label",
|
||||
title: "poll.show-results.title",
|
||||
icon: "far-eye",
|
||||
|
@ -492,7 +494,7 @@ createWidget("discourse-poll-buttons", {
|
|||
if (!attrs.isAutomaticallyClosed) {
|
||||
contents.push(
|
||||
this.attach("button", {
|
||||
className: "btn toggle-status",
|
||||
className: "btn btn-default toggle-status",
|
||||
label: "poll.open.label",
|
||||
title: "poll.open.title",
|
||||
icon: "unlock-alt",
|
||||
|
|
|
@ -59,6 +59,10 @@ div.poll {
|
|||
margin: 0.25em 0;
|
||||
color: $primary-medium;
|
||||
}
|
||||
.info-text + .info-text,
|
||||
button + .info-text {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-voters:not(:empty) {
|
||||
|
|
|
@ -72,8 +72,8 @@ en:
|
|||
confirm: "Are you sure you want to close this poll?"
|
||||
|
||||
automatic_close:
|
||||
closes_in: "closes in <strong>%{timeLeft}</strong>"
|
||||
age: "closed <strong>%{age}</strong>"
|
||||
closes_in: "Closes in <strong>%{timeLeft}</strong>."
|
||||
age: "Closed <strong>%{age}</strong>"
|
||||
|
||||
error_while_toggling_status: "Sorry, there was an error toggling the status of this poll."
|
||||
error_while_casting_votes: "Sorry, there was an error casting your votes."
|
||||
|
|
|
@ -151,7 +151,7 @@ class BulkImport::DiscourseMerger < BulkImport::Base
|
|||
copy_model(c, skip_if_merged: true, is_a_user_model: true, skip_processing: true)
|
||||
end
|
||||
|
||||
[UserAssociatedAccount, GithubUserInfo, GoogleUserInfo, Oauth2UserInfo,
|
||||
[UserAssociatedAccount, GithubUserInfo, Oauth2UserInfo,
|
||||
SingleSignOnRecord, EmailChangeRequest
|
||||
].each do |c|
|
||||
copy_model(c, skip_if_merged: true, is_a_user_model: true)
|
||||
|
@ -628,11 +628,6 @@ class BulkImport::DiscourseMerger < BulkImport::Base
|
|||
r
|
||||
end
|
||||
|
||||
def process_google_user_info(r)
|
||||
return nil if GoogleUserInfo.where(google_user_id: r['google_user_id']).exists?
|
||||
r
|
||||
end
|
||||
|
||||
def process_oauth2_user_info(r)
|
||||
return nil if Oauth2UserInfo.where(uid: r['uid'], provider: r['provider']).exists?
|
||||
r
|
||||
|
|
|
@ -106,6 +106,7 @@ class DiscourseCLI < Thor
|
|||
end
|
||||
|
||||
desc "restore", "Restore a Discourse backup"
|
||||
option :disable_emails, type: :boolean, default: true
|
||||
def restore(filename = nil)
|
||||
|
||||
if File.exist?('/usr/local/bin/discourse')
|
||||
|
@ -132,7 +133,11 @@ class DiscourseCLI < Thor
|
|||
|
||||
begin
|
||||
puts "Starting restore: #{filename}"
|
||||
restorer = BackupRestore::Restorer.new(Discourse.system_user.id, filename: filename)
|
||||
restorer = BackupRestore::Restorer.new(
|
||||
Discourse.system_user.id,
|
||||
filename: filename,
|
||||
disable_emails: options[:disable_emails]
|
||||
)
|
||||
restorer.run
|
||||
puts 'Restore done.'
|
||||
rescue BackupRestore::FilenameMissingError
|
||||
|
|
|
@ -563,7 +563,7 @@ class ImportScripts::Base
|
|||
post_creator = PostCreator.new(user, opts)
|
||||
post = post_creator.create
|
||||
post_create_action.try(:call, post) if post
|
||||
post ? post : post_creator.errors.full_messages
|
||||
post && post_creator.errors.empty? ? post : post_creator.errors.full_messages
|
||||
end
|
||||
|
||||
def create_upload(user_id, path, source_filename)
|
||||
|
|
|
@ -144,6 +144,7 @@ class ImportScripts::NodeBB < ImportScripts::Base
|
|||
suspended_till: suspended_till,
|
||||
primary_group_id: group_id_from_imported_group_id(user["groupTitle"]),
|
||||
created_at: user["joindate"],
|
||||
active: true,
|
||||
custom_fields: {
|
||||
import_pass: user["password"]
|
||||
},
|
||||
|
@ -197,13 +198,14 @@ class ImportScripts::NodeBB < ImportScripts::Base
|
|||
|
||||
upload = UploadCreator.new(file, filename).create_for(imported_user.id)
|
||||
else
|
||||
# remove "/assets/uploads/" from attachment
|
||||
# remove "/assets/uploads/" and "/uploads" from attachment
|
||||
picture = picture.gsub("/assets/uploads", "")
|
||||
picture = picture.gsub("/uploads", "")
|
||||
filepath = File.join(ATTACHMENT_DIR, picture)
|
||||
filename = File.basename(picture)
|
||||
|
||||
unless File.exists?(filepath)
|
||||
puts "Avatar file doesn't exist: #{filename}"
|
||||
puts "Avatar file doesn't exist: #{filepath}"
|
||||
return nil
|
||||
end
|
||||
|
||||
|
@ -256,13 +258,14 @@ class ImportScripts::NodeBB < ImportScripts::Base
|
|||
|
||||
upload = UploadCreator.new(file, filename).create_for(imported_user.id)
|
||||
else
|
||||
# remove "/assets/uploads/" from attachment
|
||||
# remove "/assets/uploads/" and "/uploads" from attachment
|
||||
picture = picture.gsub("/assets/uploads", "")
|
||||
picture = picture.gsub("/uploads", "")
|
||||
filepath = File.join(ATTACHMENT_DIR, picture)
|
||||
filename = File.basename(picture)
|
||||
|
||||
unless File.exists?(filepath)
|
||||
puts "Background file doesn't exist: #{filename}"
|
||||
puts "Background file doesn't exist: #{filepath}"
|
||||
return nil
|
||||
end
|
||||
|
||||
|
@ -509,13 +512,6 @@ class ImportScripts::NodeBB < ImportScripts::Base
|
|||
end
|
||||
end
|
||||
|
||||
# @username with dash to underscore
|
||||
raw = raw.gsub(/@([a-zA-Z0-9-]+)/) do
|
||||
username = $1
|
||||
|
||||
username.gsub('-', '_')
|
||||
end
|
||||
|
||||
raw
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ describe Auth::GoogleOAuth2Authenticator do
|
|||
user = Fabricate(:user)
|
||||
|
||||
hash = {
|
||||
provider: "google_oauth2",
|
||||
uid: "123456789",
|
||||
info: {
|
||||
name: "John Doe",
|
||||
|
@ -35,6 +36,7 @@ describe Auth::GoogleOAuth2Authenticator do
|
|||
user = Fabricate(:user)
|
||||
|
||||
hash = {
|
||||
provider: "google_oauth2",
|
||||
uid: "123456789",
|
||||
info: {
|
||||
name: "John Doe",
|
||||
|
@ -59,9 +61,10 @@ describe Auth::GoogleOAuth2Authenticator do
|
|||
user1 = Fabricate(:user)
|
||||
user2 = Fabricate(:user)
|
||||
|
||||
GoogleUserInfo.create!(user_id: user1.id, google_user_id: 100)
|
||||
UserAssociatedAccount.create!(provider_name: "google_oauth2", user_id: user1.id, provider_uid: 100)
|
||||
|
||||
hash = {
|
||||
provider: "google_oauth2",
|
||||
uid: "100",
|
||||
info: {
|
||||
name: "John Doe",
|
||||
|
@ -79,14 +82,17 @@ describe Auth::GoogleOAuth2Authenticator do
|
|||
result = authenticator.after_authenticate(hash, existing_account: user2)
|
||||
|
||||
expect(result.user.id).to eq(user2.id)
|
||||
expect(GoogleUserInfo.exists?(user_id: user1.id)).to eq(false)
|
||||
expect(GoogleUserInfo.exists?(user_id: user2.id)).to eq(true)
|
||||
expect(UserAssociatedAccount.exists?(user_id: user1.id)).to eq(false)
|
||||
expect(UserAssociatedAccount.exists?(user_id: user2.id)).to eq(true)
|
||||
end
|
||||
|
||||
it 'can create a proper result for non existing users' do
|
||||
hash = {
|
||||
provider: "google_oauth2",
|
||||
uid: "123456789",
|
||||
info: {
|
||||
first_name: "Jane",
|
||||
last_name: "Doe",
|
||||
name: "Jane Doe",
|
||||
email: "jane.doe@the.google.com"
|
||||
},
|
||||
|
@ -103,7 +109,7 @@ describe Auth::GoogleOAuth2Authenticator do
|
|||
result = authenticator.after_authenticate(hash)
|
||||
|
||||
expect(result.user).to eq(nil)
|
||||
expect(result.extra_data[:name]).to eq("Jane Doe")
|
||||
expect(result.name).to eq("Jane Doe")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -116,7 +122,7 @@ describe Auth::GoogleOAuth2Authenticator do
|
|||
end
|
||||
|
||||
it 'revokes correctly' do
|
||||
GoogleUserInfo.create!(user_id: user.id, google_user_id: 12345, email: 'someuser@somedomain.tld')
|
||||
UserAssociatedAccount.create!(provider_name: "google_oauth2", user_id: user.id, provider_uid: 12345)
|
||||
expect(authenticator.can_revoke?).to eq(true)
|
||||
expect(authenticator.revoke(user)).to eq(true)
|
||||
expect(authenticator.description_for_user(user)).to eq("")
|
||||
|
|
|
@ -251,6 +251,10 @@ describe Email::Receiver do
|
|||
)
|
||||
end
|
||||
|
||||
let :topic_user do
|
||||
TopicUser.find_by(topic_id: topic.id, user_id: user.id)
|
||||
end
|
||||
|
||||
it "uses MD5 of 'mail_string' there is no message_id" do
|
||||
mail_string = email(:missing_message_id)
|
||||
expect { Email::Receiver.new(mail_string).process! }.to change { IncomingEmail.count }
|
||||
|
@ -285,14 +289,34 @@ describe Email::Receiver do
|
|||
expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicNotFoundError)
|
||||
end
|
||||
|
||||
it "raises a TopicClosedError when the topic was closed" do
|
||||
topic.update_columns(closed: true)
|
||||
expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicClosedError)
|
||||
end
|
||||
context "a closed topic" do
|
||||
|
||||
it "does not raise TopicClosedError when performing a like action" do
|
||||
topic.update_columns(closed: true)
|
||||
expect { process(:like) }.to change(PostAction, :count)
|
||||
before do
|
||||
topic.update_columns(closed: true)
|
||||
end
|
||||
|
||||
it "raises a TopicClosedError when the topic was closed" do
|
||||
expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicClosedError)
|
||||
end
|
||||
|
||||
it "Can watch topics via the watch command" do
|
||||
# TODO support other locales as well, the tricky thing is that these string live in
|
||||
# client.yml not on server yml so it is a bit tricky to find
|
||||
|
||||
topic.update_columns(closed: true)
|
||||
process(:watch)
|
||||
expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:watching])
|
||||
end
|
||||
|
||||
it "Can mute topics via the mute command" do
|
||||
process(:mute)
|
||||
expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:muted])
|
||||
end
|
||||
|
||||
it "can track a topic via the track command" do
|
||||
process(:track)
|
||||
expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:tracking])
|
||||
end
|
||||
end
|
||||
|
||||
it "raises an InvalidPost when there was an error while creating the post" do
|
||||
|
|
|
@ -776,6 +776,28 @@ describe PostCreator do
|
|||
|
||||
expect(post.topic.topic_allowed_users.where(user_id: admin2.id).count).to eq(0)
|
||||
end
|
||||
|
||||
it 'does not increase posts count for small actions' do
|
||||
topic = Fabricate(:private_message_topic, user: Fabricate(:user))
|
||||
|
||||
Fabricate(:post, topic: topic)
|
||||
|
||||
1.upto(3) do |i|
|
||||
user = Fabricate(:user)
|
||||
topic.invite(topic.user, user.username)
|
||||
topic.reload
|
||||
expect(topic.posts_count).to eq(1)
|
||||
expect(topic.posts.where(post_type: Post.types[:small_action]).count).to eq(i)
|
||||
end
|
||||
|
||||
Fabricate(:post, topic: topic)
|
||||
Topic.reset_highest(topic.id)
|
||||
expect(topic.reload.posts_count).to eq(2)
|
||||
|
||||
Fabricate(:post, topic: topic)
|
||||
Topic.reset_all_highest!
|
||||
expect(topic.reload.posts_count).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
context "warnings" do
|
||||
|
@ -1238,4 +1260,32 @@ describe PostCreator do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#create_post_notice" do
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:new_user) { Fabricate(:user) }
|
||||
let(:returning_user) { Fabricate(:user) }
|
||||
|
||||
it "generates post notices" do
|
||||
# new users
|
||||
post = PostCreator.create(new_user, title: "one of my first topics", raw: "one of my first posts")
|
||||
expect(post.custom_fields["post_notice_type"]).to eq("first")
|
||||
post = PostCreator.create(new_user, title: "another one of my first topics", raw: "another one of my first posts")
|
||||
expect(post.custom_fields["post_notice_type"]).to eq(nil)
|
||||
|
||||
# returning users
|
||||
SiteSetting.returning_users_days = 30
|
||||
old_post = Fabricate(:post, user: returning_user, created_at: 31.days.ago)
|
||||
post = PostCreator.create(returning_user, title: "this is a returning topic", raw: "this is a post")
|
||||
expect(post.custom_fields["post_notice_type"]).to eq("returning")
|
||||
expect(post.custom_fields["post_notice_time"]).to eq(old_post.created_at.iso8601)
|
||||
end
|
||||
|
||||
it "does not generate post notices" do
|
||||
Fabricate(:post, user: user, created_at: 3.days.ago)
|
||||
post = PostCreator.create(user, title: "this is another topic", raw: "this is my another post")
|
||||
expect(post.custom_fields["post_notice_type"]).to eq(nil)
|
||||
expect(post.custom_fields["post_notice_time"]).to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,9 @@ describe MaxEmojisValidator do
|
|||
shared_examples "validating any topic title" do
|
||||
it 'adds an error when emoji count is greater than SiteSetting.max_emojis_in_title' do
|
||||
SiteSetting.max_emojis_in_title = 3
|
||||
record.title = '🧐 Lots of emojis here 🎃 :joy: :)'
|
||||
CustomEmoji.create!(name: 'trout', upload: Fabricate(:upload))
|
||||
Emoji.clear_cache
|
||||
record.title = '🧐 Lots of emojis here 🎃 :trout: :)'
|
||||
validate
|
||||
expect(record.errors[:title][0]).to eq(I18n.t("errors.messages.max_emojis", max_emojis_count: 3))
|
||||
|
||||
|
|
10
spec/fixtures/emails/mute.eml
vendored
Normal file
10
spec/fixtures/emails/mute.eml
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
Return-Path: <discourse@bar.com>
|
||||
From: Foo Bar <discourse@bar.com>
|
||||
To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com
|
||||
Date: Fri, 15 Jan 2016 00:12:43 +0100
|
||||
Message-ID: <13@foo.bar.mail>
|
||||
Mime-Version: 1.0
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
mute
|
10
spec/fixtures/emails/track.eml
vendored
Normal file
10
spec/fixtures/emails/track.eml
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
Return-Path: <discourse@bar.com>
|
||||
From: Foo Bar <discourse@bar.com>
|
||||
To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com
|
||||
Date: Fri, 15 Jan 2016 00:12:43 +0100
|
||||
Message-ID: <13@foo.bar.mail>
|
||||
Mime-Version: 1.0
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
track
|
10
spec/fixtures/emails/watch.eml
vendored
Normal file
10
spec/fixtures/emails/watch.eml
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
Return-Path: <discourse@bar.com>
|
||||
From: Foo Bar <discourse@bar.com>
|
||||
To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com
|
||||
Date: Fri, 15 Jan 2016 00:12:43 +0100
|
||||
Message-ID: <13@foo.bar.mail>
|
||||
Mime-Version: 1.0
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
watch
|
|
@ -38,13 +38,13 @@ describe Jobs::InvalidateInactiveAdmins do
|
|||
before do
|
||||
GithubUserInfo.create!(user_id: not_seen_admin.id, screen_name: 'bob', github_user_id: 100)
|
||||
UserOpenId.create!(url: 'https://me.yahoo.com/id/123' , user_id: not_seen_admin.id, email: 'bob@example.com', active: true)
|
||||
GoogleUserInfo.create!(user_id: not_seen_admin.id, google_user_id: 100, email: 'bob@example.com')
|
||||
UserAssociatedAccount.create!(provider_name: "google_oauth2", user_id: not_seen_admin.id, provider_uid: 100, info: { email: "bob@google.account.com" })
|
||||
end
|
||||
|
||||
it 'removes the social logins' do
|
||||
subject
|
||||
expect(GithubUserInfo.where(user_id: not_seen_admin.id).exists?).to eq(false)
|
||||
expect(GoogleUserInfo.where(user_id: not_seen_admin.id).exists?).to eq(false)
|
||||
expect(UserAssociatedAccount.where(user_id: not_seen_admin.id).exists?).to eq(false)
|
||||
expect(UserOpenId.where(user_id: not_seen_admin.id).exists?).to eq(false)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -221,9 +221,9 @@ describe Group do
|
|||
end
|
||||
|
||||
describe '.refresh_automatic_group!' do
|
||||
it "makes sure the everyone group is not visible" do
|
||||
it "makes sure the everyone group is not visible except to staff" do
|
||||
g = Group.refresh_automatic_group!(:everyone)
|
||||
expect(g.visibility_level).to eq(Group.visibility_levels[:owners])
|
||||
expect(g.visibility_level).to eq(Group.visibility_levels[:staff])
|
||||
end
|
||||
|
||||
it "ensures that the moderators group is messageable by all" do
|
||||
|
|
|
@ -477,16 +477,21 @@ describe Invite do
|
|||
|
||||
end
|
||||
|
||||
describe '.rescind_all_invites_from' do
|
||||
it 'removes all invites sent by a user' do
|
||||
describe '.rescind_all_expired_invites_from' do
|
||||
it 'removes all expired invites sent by a user' do
|
||||
SiteSetting.invite_expiry_days = 1
|
||||
user = Fabricate(:user)
|
||||
invite_1 = Fabricate(:invite, invited_by: user)
|
||||
invite_2 = Fabricate(:invite, invited_by: user)
|
||||
Invite.rescind_all_invites_from(user)
|
||||
expired_invite = Fabricate(:invite, invited_by: user)
|
||||
expired_invite.update!(created_at: 2.days.ago)
|
||||
Invite.rescind_all_expired_invites_from(user)
|
||||
invite_1.reload
|
||||
invite_2.reload
|
||||
expect(invite_1.deleted_at).to be_present
|
||||
expect(invite_2.deleted_at).to be_present
|
||||
expired_invite.reload
|
||||
expect(invite_1.deleted_at).to eq(nil)
|
||||
expect(invite_2.deleted_at).to eq(nil)
|
||||
expect(expired_invite.deleted_at).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -134,6 +134,29 @@ describe Post do
|
|||
end
|
||||
end
|
||||
|
||||
context 'a post with notices' do
|
||||
let(:post) {
|
||||
post = Fabricate(:post, post_args)
|
||||
post.custom_fields["post_notice_type"] = "returning"
|
||||
post.custom_fields["post_notice_time"] = 1.day.ago
|
||||
post
|
||||
}
|
||||
|
||||
before do
|
||||
post.trash!
|
||||
post.reload
|
||||
end
|
||||
|
||||
describe 'recovery' do
|
||||
it 'deletes notices' do
|
||||
post.recover!
|
||||
|
||||
expect(post.custom_fields).not_to have_key("post_notice_type")
|
||||
expect(post.custom_fields).not_to have_key("post_notice_time")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'flagging helpers' do
|
||||
|
|
|
@ -310,6 +310,18 @@ HTML
|
|||
|
||||
scss, _map = Stylesheet::Compiler.compile('@import "theme_variables"; @import "desktop_theme"; ', "theme.scss", theme_id: theme.id)
|
||||
expect(scss).to include("font-size:30px")
|
||||
|
||||
# Escapes correctly. If not, compiling this would throw an exception
|
||||
setting.value = <<~MULTILINE
|
||||
\#{$fakeinterpolatedvariable}
|
||||
andanothervalue 'withquotes'; margin: 0;
|
||||
MULTILINE
|
||||
|
||||
theme.set_field(target: :common, name: :scss, value: 'body {font-size: quote($font-size)}')
|
||||
theme.save!
|
||||
|
||||
scss, _map = Stylesheet::Compiler.compile('@import "theme_variables"; @import "desktop_theme"; ', "theme.scss", theme_id: theme.id)
|
||||
expect(scss).to include('font-size:"#{$fakeinterpolatedvariable}\a andanothervalue \'withquotes\'; margin: 0;\a"')
|
||||
end
|
||||
|
||||
it "allows values to be used in JS" do
|
||||
|
|
|
@ -428,7 +428,7 @@ describe User do
|
|||
UserAssociatedAccount.create(user_id: user.id, provider_name: "twitter", provider_uid: "1", info: { nickname: "sam" })
|
||||
UserAssociatedAccount.create(user_id: user.id, provider_name: "facebook", provider_uid: "1234", info: { email: "test@example.com" })
|
||||
UserAssociatedAccount.create(user_id: user.id, provider_name: "instagram", provider_uid: "examplel123123", info: { nickname: "sam" })
|
||||
GoogleUserInfo.create(user_id: user.id, email: "sam@sam.com", google_user_id: 1)
|
||||
UserAssociatedAccount.create(user_id: user.id, provider_name: "google_oauth2", provider_uid: "1", info: { email: "sam@sam.com" })
|
||||
GithubUserInfo.create(user_id: user.id, screen_name: "sam", github_user_id: 1)
|
||||
|
||||
user.reload
|
||||
|
|
|
@ -257,6 +257,33 @@ describe WebHook do
|
|||
expect(payload["id"]).to eq(post.topic.id)
|
||||
end
|
||||
|
||||
it 'should enqueue the destroyed hooks with tag filter for post events' do
|
||||
tag = Fabricate(:tag)
|
||||
Fabricate(:web_hook, tags: [tag])
|
||||
|
||||
post = PostCreator.create!(user,
|
||||
raw: 'post',
|
||||
topic_id: topic.id,
|
||||
reply_to_post_number: 1,
|
||||
skip_validations: true
|
||||
)
|
||||
|
||||
topic.tags = [tag]
|
||||
topic.save!
|
||||
|
||||
Jobs::EmitWebHookEvent.jobs.clear
|
||||
PostDestroyer.new(user, post).destroy
|
||||
|
||||
job = Jobs::EmitWebHookEvent.new
|
||||
job.expects(:web_hook_request).times(2)
|
||||
|
||||
args = Jobs::EmitWebHookEvent.jobs[1]["args"].first
|
||||
job.execute(args.with_indifferent_access)
|
||||
|
||||
args = Jobs::EmitWebHookEvent.jobs[2]["args"].first
|
||||
job.execute(args.with_indifferent_access)
|
||||
end
|
||||
|
||||
it 'should enqueue the right hooks for user events' do
|
||||
Fabricate(:user_web_hook, active: true)
|
||||
|
||||
|
|
|
@ -214,6 +214,21 @@ RSpec.describe Admin::EmailTemplatesController do
|
|||
end
|
||||
end
|
||||
|
||||
context "when subject has plural keys" do
|
||||
it "doesn't update the subject" do
|
||||
old_subject = I18n.t('system_messages.pending_users_reminder.subject_template')
|
||||
expect(old_subject).to be_a(Hash)
|
||||
|
||||
put '/admin/customize/email_templates/system_messages.pending_users_reminder', params: {
|
||||
email_template: { subject: '', body: 'Lorem ipsum' }
|
||||
}, headers: headers
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
expect(I18n.t('system_messages.pending_users_reminder.subject_template')).to eq(old_subject)
|
||||
expect(I18n.t('system_messages.pending_users_reminder.text_body_template')).to eq('Lorem ipsum')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -96,7 +96,9 @@ RSpec.describe Users::OmniauthCallbacksController do
|
|||
uid: '123545',
|
||||
info: OmniAuth::AuthHash::InfoHash.new(
|
||||
email: email,
|
||||
name: 'Some name'
|
||||
name: 'Some name',
|
||||
first_name: "Some",
|
||||
last_name: "name"
|
||||
),
|
||||
extra: {
|
||||
raw_info: OmniAuth::AuthHash.new(
|
||||
|
@ -107,7 +109,7 @@ RSpec.describe Users::OmniauthCallbacksController do
|
|||
gender: 'male',
|
||||
name: "Some name Huh",
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2]
|
||||
|
@ -262,7 +264,7 @@ RSpec.describe Users::OmniauthCallbacksController do
|
|||
@sso.return_sso_url = "http://somewhere.over.rainbow/sso"
|
||||
cookies[:sso_payload] = @sso.payload
|
||||
|
||||
GoogleUserInfo.create!(google_user_id: '12345', user: user)
|
||||
UserAssociatedAccount.create!(provider_name: "google_oauth2", provider_uid: '12345', user: user)
|
||||
|
||||
OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new(
|
||||
provider: 'google_oauth2',
|
||||
|
@ -299,7 +301,7 @@ RSpec.describe Users::OmniauthCallbacksController do
|
|||
|
||||
context 'when user has not verified his email' do
|
||||
before do
|
||||
GoogleUserInfo.create!(google_user_id: '12345', user: user)
|
||||
UserAssociatedAccount.create!(provider_name: "google_oauth2", provider_uid: '12345', user: user)
|
||||
user.update!(active: false)
|
||||
|
||||
OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new(
|
||||
|
@ -341,8 +343,8 @@ RSpec.describe Users::OmniauthCallbacksController do
|
|||
context 'when attempting reconnect' do
|
||||
let(:user2) { Fabricate(:user) }
|
||||
before do
|
||||
GoogleUserInfo.create!(google_user_id: '12345', user: user)
|
||||
GoogleUserInfo.create!(google_user_id: '123456', user: user2)
|
||||
UserAssociatedAccount.create!(provider_name: "google_oauth2", provider_uid: '12345', user: user)
|
||||
UserAssociatedAccount.create!(provider_name: "google_oauth2", provider_uid: '123456', user: user2)
|
||||
|
||||
OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new(
|
||||
provider: 'google_oauth2',
|
||||
|
@ -385,7 +387,7 @@ RSpec.describe Users::OmniauthCallbacksController do
|
|||
get "/auth/google_oauth2/callback.json"
|
||||
expect(response.status).to eq(200)
|
||||
expect(session[:current_user_id]).to eq(user2.id)
|
||||
expect(GoogleUserInfo.count).to eq(2)
|
||||
expect(UserAssociatedAccount.count).to eq(2)
|
||||
end
|
||||
|
||||
it 'should reconnect if parameter supplied' do
|
||||
|
@ -402,7 +404,7 @@ RSpec.describe Users::OmniauthCallbacksController do
|
|||
expect(session[:auth_reconnect]).to eq(nil)
|
||||
|
||||
# Disconnect
|
||||
GoogleUserInfo.find_by(user_id: user.id).destroy
|
||||
UserAssociatedAccount.find_by(user_id: user.id).destroy
|
||||
|
||||
# Reconnect flow:
|
||||
get "/auth/google_oauth2?reconnect=true"
|
||||
|
@ -414,7 +416,7 @@ RSpec.describe Users::OmniauthCallbacksController do
|
|||
expect(response.status).to eq(200)
|
||||
expect(JSON.parse(response.body)["authenticated"]).to eq(true)
|
||||
expect(session[:current_user_id]).to eq(user.id)
|
||||
expect(GoogleUserInfo.count).to eq(1)
|
||||
expect(UserAssociatedAccount.count).to eq(1)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -248,15 +248,22 @@ describe PostsController do
|
|||
let(:moderator) { Fabricate(:moderator) }
|
||||
|
||||
before do
|
||||
sign_in(moderator)
|
||||
PostAction.act(moderator, post1, PostActionType.types[:off_topic])
|
||||
PostAction.act(moderator, post2, PostActionType.types[:off_topic])
|
||||
Jobs::SendSystemMessage.clear
|
||||
end
|
||||
|
||||
it "defers the posts" do
|
||||
sign_in(moderator)
|
||||
it "defers the child posts by default" do
|
||||
expect(PostAction.flagged_posts_count).to eq(2)
|
||||
delete "/posts/destroy_many.json", params: { post_ids: [post1.id, post2.id], defer_flags: true }
|
||||
delete "/posts/destroy_many.json", params: { post_ids: [post1.id, post2.id] }
|
||||
expect(Jobs::SendSystemMessage.jobs.size).to eq(1)
|
||||
expect(PostAction.flagged_posts_count).to eq(0)
|
||||
end
|
||||
|
||||
it "can defer all posts based on `agree_with_first_reply_flag` param" do
|
||||
expect(PostAction.flagged_posts_count).to eq(2)
|
||||
delete "/posts/destroy_many.json", params: { post_ids: [post1.id, post2.id], agree_with_first_reply_flag: false }
|
||||
expect(Jobs::SendSystemMessage.jobs.size).to eq(0)
|
||||
expect(PostAction.flagged_posts_count).to eq(0)
|
||||
end
|
||||
|
|
|
@ -190,7 +190,6 @@ describe UserAnonymizer do
|
|||
end
|
||||
|
||||
it "removes external auth assocations" do
|
||||
user.google_user_info = GoogleUserInfo.create(user_id: user.id, google_user_id: "google@gmail.com")
|
||||
user.github_user_info = GithubUserInfo.create(user_id: user.id, screen_name: "example", github_user_id: "examplel123123")
|
||||
user.user_associated_accounts = [UserAssociatedAccount.create(user_id: user.id, provider_uid: "example", provider_name: "facebook")]
|
||||
user.single_sign_on_record = SingleSignOnRecord.create(user_id: user.id, external_id: "example", last_payload: "looks good")
|
||||
|
@ -198,7 +197,6 @@ describe UserAnonymizer do
|
|||
UserOpenId.create(user_id: user.id, email: user.email, url: "http://example.com/openid", active: true)
|
||||
make_anonymous
|
||||
user.reload
|
||||
expect(user.google_user_info).to eq(nil)
|
||||
expect(user.github_user_info).to eq(nil)
|
||||
expect(user.user_associated_accounts).to be_empty
|
||||
expect(user.single_sign_on_record).to eq(nil)
|
||||
|
|
|
@ -978,7 +978,6 @@ describe UserMerger do
|
|||
it "deletes external auth infos of source user" do
|
||||
UserAssociatedAccount.create(user_id: source_user.id, provider_name: "facebook", provider_uid: "1234")
|
||||
GithubUserInfo.create(user_id: source_user.id, screen_name: "example", github_user_id: "examplel123123")
|
||||
GoogleUserInfo.create(user_id: source_user.id, google_user_id: "google@gmail.com")
|
||||
Oauth2UserInfo.create(user_id: source_user.id, uid: "example", provider: "example")
|
||||
SingleSignOnRecord.create(user_id: source_user.id, external_id: "example", last_payload: "looks good")
|
||||
UserOpenId.create(user_id: source_user.id, email: source_user.email, url: "http://example.com/openid", active: true)
|
||||
|
@ -987,7 +986,6 @@ describe UserMerger do
|
|||
|
||||
expect(UserAssociatedAccount.where(user_id: source_user.id).count).to eq(0)
|
||||
expect(GithubUserInfo.where(user_id: source_user.id).count).to eq(0)
|
||||
expect(GoogleUserInfo.where(user_id: source_user.id).count).to eq(0)
|
||||
expect(Oauth2UserInfo.where(user_id: source_user.id).count).to eq(0)
|
||||
expect(SingleSignOnRecord.where(user_id: source_user.id).count).to eq(0)
|
||||
expect(UserOpenId.where(user_id: source_user.id).count).to eq(0)
|
||||
|
|
|
@ -22,7 +22,27 @@ describe UserUpdater do
|
|||
expect(MutedUser.where(user_id: u2.id).count).to eq 2
|
||||
expect(MutedUser.where(user_id: u1.id).count).to eq 2
|
||||
expect(MutedUser.where(user_id: u3.id).count).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update_ignored_users' do
|
||||
it 'updates ignored users' do
|
||||
u1 = Fabricate(:user)
|
||||
u2 = Fabricate(:user)
|
||||
u3 = Fabricate(:user)
|
||||
|
||||
updater = UserUpdater.new(u1, u1)
|
||||
updater.update_ignored_users("#{u2.username},#{u3.username}")
|
||||
|
||||
updater = UserUpdater.new(u2, u2)
|
||||
updater.update_ignored_users("#{u3.username},#{u1.username}")
|
||||
|
||||
updater = UserUpdater.new(u3, u3)
|
||||
updater.update_ignored_users("")
|
||||
|
||||
expect(IgnoredUser.where(user_id: u2.id).count).to eq 2
|
||||
expect(IgnoredUser.where(user_id: u1.id).count).to eq 2
|
||||
expect(IgnoredUser.where(user_id: u3.id).count).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -600,6 +600,24 @@ QUnit.test("Checks for existing draft", async assert => {
|
|||
toggleCheckDraftPopup(false);
|
||||
});
|
||||
|
||||
QUnit.test("Loading draft also replaces the recipients", async assert => {
|
||||
toggleCheckDraftPopup(true);
|
||||
|
||||
// prettier-ignore
|
||||
server.get("/draft.json", () => { // eslint-disable-line no-undef
|
||||
return [ 200, { "Content-Type": "application/json" }, {
|
||||
"draft":"{\"reply\":\"hello\",\"action\":\"privateMessage\",\"title\":\"hello\",\"categoryId\":null,\"archetypeId\":\"private_message\",\"metaData\":null,\"usernames\":\"codinghorror\",\"composerTime\":9159,\"typingTime\":2500}",
|
||||
"draft_sequence":0
|
||||
} ];
|
||||
});
|
||||
|
||||
await visit("/u/charlie");
|
||||
await click("button.compose-pm");
|
||||
await click(".modal .btn-default");
|
||||
|
||||
assert.equal(find(".users-input .item:eq(0)").text(), "codinghorror");
|
||||
});
|
||||
|
||||
const assertImageResized = (assert, uploads) => {
|
||||
assert.equal(
|
||||
find(".d-editor-input").val(),
|
||||
|
|
|
@ -2276,5 +2276,334 @@ export default {
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/u/charlie.json": {
|
||||
user_badges: [
|
||||
{
|
||||
id: 17,
|
||||
granted_at: "2019-03-06T19:08:28.230Z",
|
||||
count: 1,
|
||||
badge_id: 3,
|
||||
user_id: 5,
|
||||
granted_by_id: -1
|
||||
}
|
||||
],
|
||||
badges: [
|
||||
{
|
||||
id: 3,
|
||||
name: "Regular",
|
||||
description:
|
||||
'\u003ca href="https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/"\u003eGranted\u003c/a\u003e recategorize, rename, followed links, wiki, more likes',
|
||||
grant_count: 3,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-user",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 4,
|
||||
system: true,
|
||||
slug: "regular",
|
||||
manually_grantable: false,
|
||||
badge_type_id: 2
|
||||
}
|
||||
],
|
||||
badge_types: [{ id: 2, name: "Silver", sort_order: 8 }],
|
||||
users: [
|
||||
{
|
||||
id: 5,
|
||||
username: "charlie",
|
||||
name: null,
|
||||
avatar_template: "/letter_avatar_proxy/v3/letter/c/d6d6ee/{size}.png",
|
||||
moderator: false,
|
||||
admin: false
|
||||
},
|
||||
{
|
||||
id: -1,
|
||||
username: "system",
|
||||
name: "system",
|
||||
avatar_template: "/user_avatar/localhost/system/{size}/2_2.png",
|
||||
moderator: true,
|
||||
admin: true
|
||||
}
|
||||
],
|
||||
user: {
|
||||
id: 5,
|
||||
username: "charlie",
|
||||
name: null,
|
||||
avatar_template: "/letter_avatar_proxy/v3/letter/c/d6d6ee/{size}.png",
|
||||
last_posted_at: null,
|
||||
last_seen_at: null,
|
||||
created_at: "2019-03-06T19:06:20.340Z",
|
||||
can_edit: true,
|
||||
can_edit_username: true,
|
||||
can_edit_email: true,
|
||||
can_edit_name: true,
|
||||
ignored: false,
|
||||
can_ignore_user: false,
|
||||
can_send_private_messages: true,
|
||||
can_send_private_message_to_user: true,
|
||||
trust_level: 3,
|
||||
moderator: false,
|
||||
admin: false,
|
||||
title: null,
|
||||
uploaded_avatar_id: null,
|
||||
badge_count: 3,
|
||||
has_title_badges: true,
|
||||
custom_fields: {},
|
||||
pending_count: 0,
|
||||
profile_view_count: 1,
|
||||
time_read: 0,
|
||||
recent_time_read: 0,
|
||||
primary_group_name: null,
|
||||
primary_group_flair_url: null,
|
||||
primary_group_flair_bg_color: null,
|
||||
primary_group_flair_color: null,
|
||||
staged: false,
|
||||
second_factor_enabled: false,
|
||||
post_count: 0,
|
||||
can_be_deleted: true,
|
||||
can_delete_all_posts: true,
|
||||
locale: null,
|
||||
muted_category_ids: [],
|
||||
watched_tags: [],
|
||||
watching_first_post_tags: [],
|
||||
tracked_tags: [],
|
||||
muted_tags: [],
|
||||
tracked_category_ids: [],
|
||||
watched_category_ids: [],
|
||||
watched_first_post_category_ids: [],
|
||||
system_avatar_upload_id: null,
|
||||
system_avatar_template:
|
||||
"/letter_avatar_proxy/v3/letter/c/d6d6ee/{size}.png",
|
||||
muted_usernames: [],
|
||||
ignored_usernames: [],
|
||||
mailing_list_posts_per_day: 0,
|
||||
can_change_bio: true,
|
||||
user_api_keys: null,
|
||||
user_auth_tokens: [],
|
||||
user_auth_token_logs: [],
|
||||
invited_by: null,
|
||||
groups: [
|
||||
{
|
||||
id: 10,
|
||||
automatic: true,
|
||||
name: "trust_level_0",
|
||||
display_name: "trust_level_0",
|
||||
user_count: 14,
|
||||
mentionable_level: 0,
|
||||
messageable_level: 0,
|
||||
visibility_level: 0,
|
||||
automatic_membership_email_domains: null,
|
||||
automatic_membership_retroactive: false,
|
||||
primary_group: false,
|
||||
title: null,
|
||||
grant_trust_level: null,
|
||||
incoming_email: null,
|
||||
has_messages: false,
|
||||
flair_url: null,
|
||||
flair_bg_color: null,
|
||||
flair_color: null,
|
||||
bio_raw: null,
|
||||
bio_cooked: null,
|
||||
public_admission: false,
|
||||
public_exit: false,
|
||||
allow_membership_requests: false,
|
||||
full_name: null,
|
||||
default_notification_level: 3,
|
||||
membership_request_template: null
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
automatic: true,
|
||||
name: "trust_level_1",
|
||||
display_name: "trust_level_1",
|
||||
user_count: 9,
|
||||
mentionable_level: 0,
|
||||
messageable_level: 0,
|
||||
visibility_level: 0,
|
||||
automatic_membership_email_domains: null,
|
||||
automatic_membership_retroactive: false,
|
||||
primary_group: false,
|
||||
title: null,
|
||||
grant_trust_level: null,
|
||||
incoming_email: null,
|
||||
has_messages: false,
|
||||
flair_url: null,
|
||||
flair_bg_color: null,
|
||||
flair_color: null,
|
||||
bio_raw: null,
|
||||
bio_cooked: null,
|
||||
public_admission: false,
|
||||
public_exit: false,
|
||||
allow_membership_requests: false,
|
||||
full_name: null,
|
||||
default_notification_level: 3,
|
||||
membership_request_template: null
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
automatic: true,
|
||||
name: "trust_level_2",
|
||||
display_name: "trust_level_2",
|
||||
user_count: 6,
|
||||
mentionable_level: 0,
|
||||
messageable_level: 0,
|
||||
visibility_level: 0,
|
||||
automatic_membership_email_domains: null,
|
||||
automatic_membership_retroactive: false,
|
||||
primary_group: false,
|
||||
title: null,
|
||||
grant_trust_level: null,
|
||||
incoming_email: null,
|
||||
has_messages: false,
|
||||
flair_url: null,
|
||||
flair_bg_color: null,
|
||||
flair_color: null,
|
||||
bio_raw: null,
|
||||
bio_cooked: null,
|
||||
public_admission: false,
|
||||
public_exit: false,
|
||||
allow_membership_requests: false,
|
||||
full_name: null,
|
||||
default_notification_level: 3,
|
||||
membership_request_template: null
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
automatic: true,
|
||||
name: "trust_level_3",
|
||||
display_name: "trust_level_3",
|
||||
user_count: 3,
|
||||
mentionable_level: 0,
|
||||
messageable_level: 0,
|
||||
visibility_level: 0,
|
||||
automatic_membership_email_domains: null,
|
||||
automatic_membership_retroactive: false,
|
||||
primary_group: false,
|
||||
title: null,
|
||||
grant_trust_level: null,
|
||||
incoming_email: null,
|
||||
has_messages: false,
|
||||
flair_url: null,
|
||||
flair_bg_color: null,
|
||||
flair_color: null,
|
||||
bio_raw: null,
|
||||
bio_cooked: null,
|
||||
public_admission: false,
|
||||
public_exit: false,
|
||||
allow_membership_requests: false,
|
||||
full_name: null,
|
||||
default_notification_level: 3,
|
||||
membership_request_template: null
|
||||
}
|
||||
],
|
||||
group_users: [
|
||||
{ group_id: 10, user_id: 5, notification_level: 3 },
|
||||
{ group_id: 11, user_id: 5, notification_level: 3 },
|
||||
{ group_id: 12, user_id: 5, notification_level: 3 },
|
||||
{ group_id: 13, user_id: 5, notification_level: 3 }
|
||||
],
|
||||
featured_user_badge_ids: [17],
|
||||
user_option: {
|
||||
user_id: 5,
|
||||
email_always: false,
|
||||
mailing_list_mode: false,
|
||||
mailing_list_mode_frequency: 1,
|
||||
email_digests: true,
|
||||
email_private_messages: true,
|
||||
email_direct: true,
|
||||
external_links_in_new_tab: false,
|
||||
dynamic_favicon: false,
|
||||
enable_quoting: true,
|
||||
disable_jump_reply: false,
|
||||
digest_after_minutes: 10080,
|
||||
automatically_unpin_topics: true,
|
||||
auto_track_topics_after_msecs: 240000,
|
||||
notification_level_when_replying: 2,
|
||||
new_topic_duration_minutes: 2880,
|
||||
email_previous_replies: 2,
|
||||
email_in_reply_to: true,
|
||||
like_notification_frequency: 1,
|
||||
include_tl0_in_digests: false,
|
||||
theme_ids: [2],
|
||||
theme_key_seq: 0,
|
||||
allow_private_messages: true,
|
||||
homepage_id: null,
|
||||
hide_profile_and_presence: false,
|
||||
text_size: "normal",
|
||||
text_size_seq: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"/u/charlie/summary.json": {
|
||||
topics: [],
|
||||
badges: [
|
||||
{
|
||||
id: 3,
|
||||
name: "Regular",
|
||||
description:
|
||||
'\u003ca href="https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/"\u003eGranted\u003c/a\u003e recategorize, rename, followed links, wiki, more likes',
|
||||
grant_count: 3,
|
||||
allow_title: true,
|
||||
multiple_grant: false,
|
||||
icon: "fa-user",
|
||||
image: null,
|
||||
listable: true,
|
||||
enabled: true,
|
||||
badge_grouping_id: 4,
|
||||
system: true,
|
||||
slug: "regular",
|
||||
manually_grantable: false,
|
||||
badge_type_id: 2
|
||||
}
|
||||
],
|
||||
badge_types: [{ id: 2, name: "Silver", sort_order: 8 }],
|
||||
users: [
|
||||
{
|
||||
id: 5,
|
||||
username: "charlie",
|
||||
name: null,
|
||||
avatar_template: "/letter_avatar_proxy/v3/letter/c/d6d6ee/{size}.png",
|
||||
moderator: false,
|
||||
admin: false
|
||||
},
|
||||
{
|
||||
id: -1,
|
||||
username: "system",
|
||||
name: "system",
|
||||
avatar_template: "/user_avatar/localhost/system/{size}/2_2.png",
|
||||
moderator: true,
|
||||
admin: true
|
||||
}
|
||||
],
|
||||
user_summary: {
|
||||
likes_given: 0,
|
||||
likes_received: 0,
|
||||
topics_entered: 0,
|
||||
posts_read_count: 0,
|
||||
days_visited: 0,
|
||||
topic_count: 0,
|
||||
post_count: 0,
|
||||
time_read: 0,
|
||||
recent_time_read: 0,
|
||||
topic_ids: [],
|
||||
replies: [],
|
||||
links: [],
|
||||
most_liked_by_users: [],
|
||||
most_liked_users: [],
|
||||
most_replied_to_users: [],
|
||||
badges: [
|
||||
{
|
||||
id: 17,
|
||||
granted_at: "2019-03-06T19:08:28.230Z",
|
||||
count: 1,
|
||||
badge_id: 3,
|
||||
user_id: 5,
|
||||
granted_by_id: -1
|
||||
}
|
||||
],
|
||||
top_categories: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -852,3 +852,22 @@ widgetTest("pm map", {
|
|||
assert.equal(find(".private-message-map .user").length, 1);
|
||||
}
|
||||
});
|
||||
|
||||
widgetTest("post notice", {
|
||||
template: '{{mount-widget widget="post" args=args}}',
|
||||
beforeEach() {
|
||||
this.set("args", {
|
||||
postNoticeType: "returning",
|
||||
postNoticeTime: new Date("2010-01-01 12:00:00 UTC"),
|
||||
username: "codinghorror"
|
||||
});
|
||||
},
|
||||
test(assert) {
|
||||
assert.equal(
|
||||
find(".post-notice")
|
||||
.text()
|
||||
.trim(),
|
||||
I18n.t("post.notice.return", { user: "codinghorror", time: "Jan '10" })
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
234
vendor/assets/svg-icons/fontawesome/brands.svg
vendored
234
vendor/assets/svg-icons/fontawesome/brands.svg
vendored
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 427 KiB After Width: | Height: | Size: 440 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user