Version bump

This commit is contained in:
Neil Lalonde 2020-06-24 14:00:44 -04:00
commit f407c88327
No known key found for this signature in database
GPG Key ID: FF871CA9037D0A91
355 changed files with 8133 additions and 2554 deletions

View File

@ -32,7 +32,7 @@ jobs:
target: ["PLUGINS", "CORE"]
os: [ubuntu-latest]
ruby: ["2.6"]
postgres: ["10"]
postgres: ["12"]
redis: ["4.x"]
services:
@ -64,9 +64,17 @@ jobs:
- name: Setup packages
if: env.BUILD_TYPE != 'LINT'
run: |
sudo apt-get update
sudo apt-get -yqq install postgresql-client libpq-dev gifsicle jpegoptim optipng jhead
wget -qO- https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-pngquant | sudo sh
- name: Update imagemagick
if: env.BUILD_TYPE == 'BACKEND'
run: |
wget https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-imagemagick
chmod +x install-imagemagick
sudo ./install-imagemagick
- name: Setup redis
uses: shogo82148/actions-setup-redis@v1
if: env.BUILD_TYPE != 'LINT'

View File

@ -1,2 +1,9 @@
inherit_gem:
rubocop-discourse: default.yml
# Still work to do in ensuring we don't link old files
Discourse/NoAddReferenceOrAliasesActiveRecordMigration:
Enabled: false
Discourse/NoResetColumnInformationInMigrations:
Enabled: true

View File

@ -29,5 +29,3 @@ Javascript
Ruby
Rails - Copyright (c) 2005-2013 David Heinemeier Hansson, Rails Core Team contributors (MIT)
Thin - Copyright (c) 2012-2013 Marc-Andre Cournoyer

View File

@ -178,7 +178,7 @@ end
group :development do
gem 'ruby-prof', require: false, platform: :mri
gem 'bullet', require: !!ENV['BULLET']
gem 'better_errors', platform: :mri
gem 'better_errors', platform: :mri, require: !!ENV['BETTER_ERRORS']
gem 'binding_of_caller'
gem 'yaml-lint'
gem 'annotate'
@ -250,4 +250,4 @@ gem 'webpush', require: false
gem 'colored2', require: false
gem 'maxminddb'
gem 'rails_failover', require: false, git: 'https://github.com/discourse/rails_failover'
gem 'rails_failover', require: false

View File

@ -1,11 +1,3 @@
GIT
remote: https://github.com/discourse/rails_failover
revision: 66602aa73785851b81c506f0023d3c2a2e40de0a
specs:
rails_failover (0.4.0)
activerecord (~> 6.0)
railties (~> 6.0)
GEM
remote: https://rubygems.org/
specs:
@ -51,10 +43,10 @@ GEM
annotate (3.1.1)
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
ast (2.4.0)
ast (2.4.1)
aws-eventstream (1.1.0)
aws-partitions (1.322.0)
aws-sdk-core (3.96.1)
aws-partitions (1.329.0)
aws-sdk-core (3.99.1)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
@ -66,11 +58,11 @@ GEM
aws-sdk-core (~> 3, >= 3.96.1)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sdk-sns (1.23.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sdk-sns (1.25.1)
aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.3)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-sigv4 (1.2.0)
aws-eventstream (~> 1, >= 1.0.2)
barber (0.12.2)
ember-source (>= 1.0, < 3.1)
execjs (>= 1.2, < 3)
@ -104,7 +96,7 @@ GEM
css_parser (1.7.1)
addressable
debug_inspector (0.0.3)
diff-lcs (1.3)
diff-lcs (1.4.1)
diffy (3.3.0)
discourse-ember-source (3.12.2.0)
discourse_image_optim (0.26.2)
@ -129,7 +121,7 @@ GEM
railties (>= 3.1)
ember-source (2.18.2)
erubi (1.9.0)
excon (0.73.0)
excon (0.75.0)
execjs (2.7.0)
exifr (1.3.6)
fabrication (2.21.1)
@ -142,7 +134,7 @@ GEM
rake-compiler
fast_xs (0.8.0)
fastimage (2.1.7)
ffi (1.13.0)
ffi (1.13.1)
flamegraph (0.9.5)
fspath (3.1.2)
gc_tracer (1.5.1)
@ -181,8 +173,8 @@ GEM
logstash-event (1.2.02)
logstash-logger (0.26.1)
logstash-event (~> 1.2)
logster (2.8.0)
loofah (2.5.0)
logster (2.9.0)
loofah (2.6.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lru_redux (1.1.0)
@ -205,7 +197,7 @@ GEM
ffi (~> 1.9)
minitest (5.14.1)
mocha (1.11.2)
mock_redis (0.23.0)
mock_redis (0.24.0)
msgpack (1.3.3)
multi_json (1.14.1)
multi_xml (0.6.0)
@ -248,7 +240,7 @@ GEM
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.9.28.3)
onebox (1.9.29)
addressable (~> 2.7.0)
htmlentities (~> 4.3)
multi_json (~> 1.11)
@ -257,11 +249,11 @@ GEM
sanitize
openssl-signature_algorithm (0.4.0)
optimist (3.0.1)
parallel (1.19.1)
parallel_tests (2.32.0)
parallel (1.19.2)
parallel_tests (3.0.0)
parallel
parser (2.7.1.3)
ast (~> 2.4.0)
parser (2.7.1.4)
ast (~> 2.4.1)
pg (1.2.3)
progress (3.5.2)
pry (0.13.1)
@ -288,6 +280,9 @@ GEM
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
rails_failover (0.5.2)
activerecord (~> 6.0)
railties (~> 6.0)
rails_multisite (2.3.0)
activerecord (> 5.0, < 7)
railties (> 5.0, < 7)
@ -310,7 +305,7 @@ GEM
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
rchardet (1.8.0)
redis (4.1.4)
redis (4.2.1)
redis-namespace (1.7.0)
redis (>= 3.0.4)
regexp_parser (1.7.1)
@ -353,21 +348,21 @@ GEM
json-schema (~> 2.2)
railties (>= 3.1, < 7.0)
rtlit (0.0.5)
rubocop (0.85.1)
rubocop (0.86.0)
parallel (~> 1.10)
parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.7)
rexml
rubocop-ast (>= 0.0.3)
rubocop-ast (>= 0.0.3, < 1.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-ast (0.0.3)
parser (>= 2.7.0.1)
rubocop-discourse (2.1.2)
rubocop-discourse (2.2.0)
rubocop (>= 0.69.0)
rubocop-rspec (>= 1.39.0)
rubocop-rspec (1.39.0)
rubocop-rspec (1.40.0)
rubocop (>= 0.68.1)
ruby-prof (1.4.1)
ruby-progressbar (1.10.1)
@ -376,7 +371,7 @@ GEM
nokogiri (>= 1.6.0)
rubyzip (2.3.0)
safe_yaml (1.0.5)
sanitize (5.1.0)
sanitize (5.2.1)
crass (~> 1.0.2)
nokogiri (>= 1.8.0)
nokogumbo (~> 2.0)
@ -526,7 +521,7 @@ DEPENDENCIES
rack (= 2.2.2)
rack-mini-profiler
rack-protection
rails_failover!
rails_failover
rails_multisite
railties (= 6.0.3.1)
rake

View File

@ -22,6 +22,16 @@ export default Controller.extend({
this.model.unshiftObject(arg);
},
copyUrl(pl) {
let linkElement = document.querySelector(`#admin-permalink-${pl.id}`);
let textArea = document.createElement("textarea");
textArea.value = linkElement.textContent;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("Copy");
textArea.remove();
},
destroy: function(record) {
return bootbox.confirm(
I18n.t("admin.permalink.delete_confirm"),

View File

@ -12,6 +12,7 @@
content=permalinkTypes
value=permalinkType
onChange=(action (mut permalinkType))
class="permalink-type"
}}
{{text-field

View File

@ -1,13 +1,13 @@
{{#admin-nav}}
{{nav-item route="adminCustomizeThemes" label="admin.customize.theme.title"}}
{{nav-item route="adminCustomize.colors" label="admin.customize.colors.title"}}
{{nav-item route="adminSiteText" label="admin.site_text.title"}}
{{nav-item route="adminCustomizeEmailTemplates" label="admin.customize.email_templates.title"}}
{{nav-item route="adminCustomizeEmailStyle" label="admin.customize.email_style.title"}}
{{nav-item route="adminUserFields" label="admin.user_fields.title"}}
{{nav-item route="adminEmojis" label="admin.emoji.title"}}
{{nav-item route="adminPermalinks" label="admin.permalink.title"}}
{{nav-item route="adminEmbedding" label="admin.embedding.title"}}
{{nav-item route="adminCustomizeThemes" label="admin.customize.theme.title" class="admin-customize-themes"}}
{{nav-item route="adminCustomize.colors" label="admin.customize.colors.title" class="admin-customize-colors"}}
{{nav-item route="adminSiteText" label="admin.site_text.title" class="admin-customize-site-text"}}
{{nav-item route="adminCustomizeEmailTemplates" label="admin.customize.email_templates.title" class="admin-customize-email-templates"}}
{{nav-item route="adminCustomizeEmailStyle" label="admin.customize.email_style.title" class="admin-customize-email-styles"}}
{{nav-item route="adminUserFields" label="admin.user_fields.title" class="admin-customize-user-fields"}}
{{nav-item route="adminEmojis" label="admin.emoji.title" class="admin-customize-emojis"}}
{{nav-item route="adminPermalinks" label="admin.permalink.title" class="admin-customize-permalinks"}}
{{nav-item route="adminEmbedding" label="admin.embedding.title" class="admin-customize-embedding"}}
{{/admin-nav}}
<div class="admin-container">

View File

@ -21,7 +21,7 @@
<tbody>
{{#each model as |pl|}}
<tr class="admin-list-item">
<td class="col first url">{{pl.url}}</td>
<td class="col first url">{{d-button title="admin.permalink.copy_to_clipboard" icon="far-clipboard" action=(action "copyUrl" pl)}} <span id="admin-permalink-{{pl.id}}" title={{pl.url}}>{{pl.url}}</span></td>
<td class="col destination">
{{#if pl.topic_id}}
<a href={{pl.topic_url}}>{{pl.topic_title}}</a>
@ -42,7 +42,7 @@
<a href={{pl.external_url}}>{{pl.external_url}}</a>
{{/if}}
</td>
<td class="col action">
<td class="col action" style="text-align: right;">
{{d-button action=(action "destroy") actionParam=pl icon="far-trash-alt" class="btn-danger"}}
</td>
</tr>

View File

@ -514,7 +514,7 @@
<div class="display-row">
<div class="field">{{i18n "admin.groups.custom"}}</div>
<div class="value">
{{admin-group-selector
{{group-chooser
content=availableGroups
value=customGroupIdsBuffer
labelProperty="name"

View File

@ -0,0 +1,5 @@
import TextArea from "@ember/component/text-area";
export default TextArea.extend({
attributeBindings: ["aria-label"]
});

View File

@ -12,7 +12,8 @@ export default TextField.extend({
"autocapitalize",
"autofocus",
"maxLength",
"dir"
"dir",
"aria-label"
],
init() {

View File

@ -180,7 +180,7 @@ export default Component.extend({
this.set("docked", isDocked);
const $replyArea = $("#reply-control .reply-area");
if ($replyArea && $replyArea.length > 0 && wrapperDir === "left") {
if ($replyArea && $replyArea.length > 0) {
$wrapper.css(wrapperDir, `${$replyArea.offset().left}px`);
} else {
$wrapper.css(wrapperDir, "1em");

View File

@ -1040,7 +1040,7 @@ export default Controller.extend({
const keyPrefix =
this.model.action === "edit" ? "post.abandon_edit" : "post.abandon";
let promise = new Promise(resolve => {
let promise = new Promise((resolve, reject) => {
if (this.get("model.hasMetaData") || this.get("model.replyDirty")) {
bootbox.dialog(I18n.t(keyPrefix + ".confirm"), [
{
@ -1052,8 +1052,10 @@ export default Controller.extend({
if (differentDraft) {
this.model.clearState();
this.close();
resolve();
}
resolve();
reject();
}
},
{

View File

@ -60,6 +60,11 @@ export default Controller.extend(ModalFunctionality, {
);
},
@discourseComputed("previousVersion")
revertToRevisionText(revision) {
return I18n.t("post.revisions.controls.revert", { revision });
},
refresh(postId, postVersion) {
this.set("loading", true);
@ -261,9 +266,10 @@ export default Controller.extend(ModalFunctionality, {
this.set("bodyDiff", html);
} else {
const opts = {
features: { editHistory: true },
features: { editHistory: true, historyOneboxes: true },
whiteListed: {
editHistory: { custom: (tag, attr) => attr === "class" }
editHistory: { custom: (tag, attr) => attr === "class" },
historyOneboxes: ["header", "article", "div[style]"]
}
};

View File

@ -35,6 +35,7 @@ export default Controller.extend(ModalFunctionality, StateHelpers, {
this.state === States.existing
);
}),
showUnpublish: computed("state", function() {
return this.state === States.existing || this.state === States.unpublishing;
}),
@ -95,7 +96,7 @@ export default Controller.extend(ModalFunctionality, StateHelpers, {
this.set("state", States.saving);
return this.publishedPage
.update({ slug: this.publishedPage.slug })
.update(this.publishedPage.getProperties("slug", "public"))
.then(() => {
this.set("state", States.existing);
this.model.set("publishedPage", this.publishedPage);
@ -110,11 +111,17 @@ export default Controller.extend(ModalFunctionality, StateHelpers, {
startNew() {
this.setProperties({
state: States.new,
publishedPage: this.store.createRecord("published_page", {
id: this.model.id,
slug: this.model.slug
})
publishedPage: this.store.createRecord(
"published_page",
this.model.getProperties("id", "slug", "public")
)
});
this.checkSlug();
},
@action
onChangePublic(isPublic) {
this.publishedPage.set("public", isPublic);
this.publish();
}
});

View File

@ -1,7 +1,5 @@
import { later } from "@ember/runloop";
import Mobile from "discourse/lib/mobile";
import { setResolverOption } from "discourse-common/resolver";
import { isAppWebview, postRNWebviewMessage } from "discourse/lib/utilities";
// Initializes the `Mobile` helper object.
export default {
@ -16,14 +14,5 @@ export default {
site.set("isMobileDevice", Mobile.isMobileDevice);
setResolverOption("mobileView", Mobile.mobileView);
if (isAppWebview()) {
later(() => {
postRNWebviewMessage(
"headerBg",
$(".d-header").css("background-color")
);
}, 500);
}
}
};

View File

@ -0,0 +1,20 @@
import { later } from "@ember/runloop";
import { isAppWebview, postRNWebviewMessage } from "discourse/lib/utilities";
// Send bg color to webview so iOS status bar matches site theme
export default {
name: "webview-background",
after: "inject-objects",
initialize() {
if (isAppWebview()) {
later(() => {
const header = document.querySelectorAll(".d-header")[0];
if (header) {
const styles = window.getComputedStyle(header);
postRNWebviewMessage("headerBg", styles.backgroundColor);
}
}, 500);
}
}
};

View File

@ -355,7 +355,8 @@ export default {
this.container.lookup("controller:topic").togglePinnedState();
},
goToPost() {
goToPost(event) {
preventKeyboardEvent(event);
this.appEvents.trigger("topic:keyboard-trigger", { type: "jump" });
},

View File

@ -30,7 +30,8 @@ const SERVER_SIDE_ONLY = [
/^\/admin\/upgrade$/,
/^\/logs($|\/)/,
/^\/admin\/logs\/watched_words\/action\/[^\/]+\/download$/,
/^\/pub\//
/^\/pub\//,
/^\/invites\//
];
export function rewritePath(path) {

View File

@ -25,6 +25,10 @@ export function translateSize(size) {
}
export function escapeExpression(string) {
if (!string) {
return "";
}
// don't escape SafeStrings, since they're already safe
if (string instanceof Handlebars.SafeString) {
return string.toString();

View File

@ -71,11 +71,16 @@ const NavItem = EmberObject.extend({
return mode + name.replace(" ", "-");
},
@discourseComputed("name", "category", "topicTrackingState.messageCount")
count(name, category) {
@discourseComputed(
"name",
"category",
"tagId",
"topicTrackingState.messageCount"
)
count(name, category, tagId) {
const state = this.topicTrackingState;
if (state) {
return state.lookupCount(name, category);
return state.lookupCount(name, category, tagId);
}
}
});

View File

@ -132,7 +132,9 @@ Site.reopenClass(Singleton, {
// The current singleton will retrieve its attributes from the `PreloadStore`.
createCurrent() {
const store = Discourse.__container__.lookup("service:store");
return store.createRecord("site", PreloadStore.get("site"));
const siteAttributes = PreloadStore.get("site");
siteAttributes["isReadOnly"] = PreloadStore.get("isReadOnly");
return store.createRecord("site", siteAttributes);
},
create() {

View File

@ -408,7 +408,7 @@ const TopicTrackingState = EmberObject.extend({
return new Set(result);
},
countCategoryByState(fn, categoryId) {
countCategoryByState(fn, categoryId, tagId) {
const subcategoryIds = this.getSubCategoryIds(categoryId);
return _.chain(this.states)
.filter(fn)
@ -416,17 +416,18 @@ const TopicTrackingState = EmberObject.extend({
topic =>
topic.archetype !== "private_message" &&
!topic.deleted &&
(!categoryId || subcategoryIds.has(topic.category_id))
(!categoryId || subcategoryIds.has(topic.category_id)) &&
(!tagId || (topic.tags && topic.tags.indexOf(tagId) > -1))
)
.value().length;
},
countNew(categoryId) {
return this.countCategoryByState(isNew, categoryId);
countNew(categoryId, tagId) {
return this.countCategoryByState(isNew, categoryId, tagId);
},
countUnread(categoryId) {
return this.countCategoryByState(isUnread, categoryId);
countUnread(categoryId, tagId) {
return this.countCategoryByState(isUnread, categoryId, tagId);
},
countTags(tags) {
@ -462,10 +463,14 @@ const TopicTrackingState = EmberObject.extend({
return counts;
},
countCategory(category_id) {
countCategory(category_id, tagId) {
let sum = 0;
Object.values(this.states).forEach(topic => {
if (topic.category_id === category_id && !topic.deleted) {
if (
topic.category_id === category_id &&
!topic.deleted &&
(!tagId || (topic.tags && topic.tags.indexOf(tagId) > -1))
) {
sum +=
topic.last_read_post_number === null ||
topic.last_read_post_number < topic.highest_post_number
@ -476,23 +481,24 @@ const TopicTrackingState = EmberObject.extend({
return sum;
},
lookupCount(name, category) {
lookupCount(name, category, tagId) {
if (name === "latest") {
return (
this.lookupCount("new", category) + this.lookupCount("unread", category)
this.lookupCount("new", category, tagId) +
this.lookupCount("unread", category, tagId)
);
}
let categoryId = category ? get(category, "id") : null;
if (name === "new") {
return this.countNew(categoryId);
return this.countNew(categoryId, tagId);
} else if (name === "unread") {
return this.countUnread(categoryId);
return this.countUnread(categoryId, tagId);
} else {
const categoryName = name.split("/")[1];
if (categoryName) {
return this.countCategory(categoryId);
return this.countCategory(categoryId, tagId);
}
}
},

View File

@ -430,8 +430,11 @@ const Topic = RestModel.extend({
return this.firstPost().then(firstPost => {
const toggleBookmarkOnServer = () => {
if (bookmark) {
return firstPost.toggleBookmark().then(() => {
return firstPost.toggleBookmark().then(opts => {
this.set("bookmarking", false);
if (opts.closedWithoutSaving) {
return;
}
return this.afterTopicBookmarked(firstPost);
});
} else {

View File

@ -595,7 +595,7 @@ const User = RestModel.extend({
);
}
if (!isEmpty(json.user.groups)) {
if (!isEmpty(json.user.groups) && !isEmpty(json.user.group_users)) {
const groups = [];
for (let i = 0; i < json.user.groups.length; i++) {
@ -770,6 +770,7 @@ const User = RestModel.extend({
this.setProperties({
email: result.email,
secondary_emails: result.secondary_emails,
unconfirmed_emails: result.unconfirmed_emails,
associated_accounts: result.associated_accounts
});
}

View File

@ -11,6 +11,7 @@
</thead>
<tbody aria-labelledby="categories-only-category">
{{#each categories as |c|}}
{{plugin-outlet name="category-list-above-each-category" connectorTagName="" tagName="" args=(hash category=c)}}
<tr data-category-id={{c.id}} data-notification-level={{c.notificationLevelString}} class="{{if c.description_excerpt "has-description" "no-description"}} {{if c.uploaded_logo.url "has-logo" "no-logo"}}">
<td class="category {{if c.isMuted "muted"}} {{if noCategoryStyle "no-category-style"}}" style={{unless noCategoryStyle (border-color c.color)}}>
{{category-title-link category=c}}

View File

@ -3,6 +3,7 @@
id="reply-title"
maxLength=titleMaxLength
placeholderKey=composer.titlePlaceholder
aria-label=(I18n composer.titlePlaceholder)
disabled=disabled
autocomplete="discourse"}}

View File

@ -33,12 +33,13 @@
</div>
{{conditional-loading-spinner condition=loading}}
{{textarea
{{d-textarea
autocomplete="discourse"
tabindex=tabindex
value=value
class="d-editor-input"
placeholder=placeholderTranslated
aria-label=placeholderTranslated
disabled=disabled
input=change}}
{{popup-input-tip validation=validation}}

View File

@ -11,10 +11,13 @@
includeWeekend=includeWeekend
includeFarFuture=includeFarFuture
includeMidFuture=includeMidFuture
includeNow=includeNow
clearable=clearable
none="topic.auto_update_input.none"
onChangeInput=onChangeInput
onChange=(action (mut selection))
options=(hash
none="topic.auto_update_input.none"
)
}}
</div>
{{/unless}}

View File

@ -15,6 +15,7 @@
<div class="name-line">
<span class="username"><a href={{this.userPath}} data-user-card={{@user.username}}>{{format-username @user.username}}</a></span>
<span class="name">{{this.name}}</span>
{{plugin-outlet name="after-user-name" connectorTagName="span" args=(hash user=user)}}
</div>
<div class="title">{{@user.title}}</div>
@ -25,3 +26,5 @@
{{/if}}
</div>
{{plugin-outlet name="after-user-info" args=(hash user=user)}}

View File

@ -10,7 +10,7 @@
messageCount=messageCount
addLinkLookup=(action "addLinkLookup")}}
{{#if model.viewOpenOrFullscreen}}
<div class="reply-area {{if canEditTags "with-tags"}}">
<div role="form" aria-label={{I18n saveLabel}} class="reply-area {{if canEditTags "with-tags"}}">
<div class="composer-fields">
{{plugin-outlet name="composer-open" args=(hash model=model)}}
<div class="reply-to">
@ -100,7 +100,7 @@
</div>
{{/if}}
{{plugin-outlet name="composer-fields" args=(hash model=model)}}
{{plugin-outlet name="composer-fields" args=(hash model=model showPreview=showPreview)}}
{{/unless}}
</div>

View File

@ -1,6 +1,7 @@
{{#if categories}}
<div class="category-list {{if showTopics "with-topics"}}">
{{#each categories as |c|}}
{{plugin-outlet name="category-list-above-each-category" connectorTagName="" tagName="" args=(hash category=c)}}
<div data-category-id={{c.id}} data-notification-level={{c.notificationLevelString}} style={{border-color c.color}} class="category-list-item category {{if c.isMuted "muted"}}">
<table class="topic-list">
<tbody>

View File

@ -23,6 +23,7 @@
{{~#if topic.featured_link~}}
{{~topic-featured-link topic~}}
{{~/if~}}
{{~raw-plugin-outlet name="topic-list-after-title"}}
{{~#if topic.unseen~}}
<span class="topic-post-badges">&nbsp;<span class="badge-notification new-topic"></span></span>
{{~/if~}}

View File

@ -9,7 +9,7 @@
{{/if}}
<div class="control-group bookmark-name-wrap">
{{input id="bookmark-name" value=model.name name="bookmark-name" class="bookmark-name" placeholder=(i18n "post.bookmarks.name_placeholder")}}
{{input id="bookmark-name" value=model.name name="bookmark-name" class="bookmark-name" enter=(action "saveAndClose") placeholder=(i18n "post.bookmarks.name_placeholder")}}
{{d-button icon="cog" action=(action "toggleOptionsPanel") class="bookmark-options-button"}}
</div>

View File

@ -128,7 +128,7 @@
{{/if}}
{{#if displayRevert}}
{{d-button action=(action "revertToVersion") icon="undo" label="post.revisions.controls.revert" class="btn-danger" disabled=loading}}
{{d-button action=(action "revertToVersion") icon="undo" translatedLabel=revertToRevisionText class="btn-danger" disabled=loading}}
{{/if}}
{{#if displayHide}}

View File

@ -12,7 +12,7 @@
includeWeekend=true
includeDateTime=false
includeMidFuture=true
includeFarFuture=false
includeFarFuture=true
onChangeInput=(action (mut ignoredUntil))
}}
<p>{{i18n "user.user_notifications.ignore_duration_note"}}</p>

View File

@ -5,7 +5,7 @@
includeWeekend=true
includeDateTime=false
includeMidFuture=true
includeFarFuture=false
includeFarFuture=true
onChangeInput=(action (mut ignoredUntil))
}}
<p>{{i18n "user.user_notifications.ignore_duration_note"}}</p>

View File

@ -6,8 +6,29 @@
<p class="publish-description">{{i18n "topic.publish_page.description"}}</p>
<form>
<label>{{i18n "topic.publish_page.slug"}}</label>
{{text-field value=publishedPage.slug onChange=(action "checkSlug") onChangeImmediate=(action "startCheckSlug") disabled=existing class="publish-slug"}}
<div class="controls">
<label>{{i18n "topic.publish_page.slug"}}</label>
{{text-field
value=publishedPage.slug
onChange=(action "checkSlug")
onChangeImmediate=(action "startCheckSlug")
disabled=existing
class="publish-slug"
}}
</div>
<div class="controls">
<label>{{i18n "topic.publish_page.public"}}</label>
<p class="description">
{{input
type="checkbox"
checked=publishedPage.public
click=(action "onChangePublic" value="target.checked")
}}
{{i18n "topic.publish_page.public_description"}}
</p>
</div>
</form>
<div class="publish-url">
@ -40,14 +61,15 @@
<div class="modal-footer">
{{#if showUnpublish}}
{{d-button icon="times" label="close" action=(action "closeModal")}}
{{d-button
label="topic.publish_page.unpublish"
icon="trash-alt"
class="btn-danger"
isLoading=unpublishing
action=(action "unpublish") }}
action=(action "unpublish")
}}
{{d-button class="close-publish-page" icon="times" label="close" action=(action "closeModal")}}
{{else if unpublished}}
{{d-button label="topic.publish_page.publishing_settings" action=(action "startNew")}}
{{else}}

View File

@ -50,9 +50,11 @@
<div class="emails">
{{#each emails as |email|}}
<div class="row email">
{{email-dropdown email=email
setPrimaryEmail=(action "setPrimaryEmail")
destroyEmail=(action "destroyEmail")}}
{{#if model.can_edit_email}}
{{email-dropdown email=email
setPrimaryEmail=(action "setPrimaryEmail")
destroyEmail=(action "destroyEmail")}}
{{/if}}
<div class="email-first">{{email.email}}</div>
@ -77,9 +79,11 @@
{{/each}}
</div>
{{#link-to "preferences.email" (query-params new=1) class="pull-right"}}
{{d-icon "plus"}} {{i18n "user.email.add_email"}}
{{/link-to}}
{{#if model.can_edit_email}}
{{#link-to "preferences.email" (query-params new=1) class="pull-right"}}
{{d-icon "plus"}} {{i18n "user.email.add_email"}}
{{/link-to}}
{{/if}}
{{else}}
<div class="controls">
<span class="static">{{model.email}}</span>

View File

@ -1,3 +1,5 @@
{{plugin-outlet name="user-preferences-interface-top" args=(hash model=model save=(action "save"))}}
{{#if showThemeSelector}}
<div class="control-group theme">
<label class="control-label">{{i18n "user.theme"}}</label>

View File

@ -90,6 +90,9 @@
{{#if model.publishedPage}}
<div class="published-page-notice">
<div class="details">
{{#if model.publishedPage.public}}
<span class="is-public">{{i18n "topic.publish_page.public"}}</span>
{{/if}}
{{i18n "topic.publish_page.topic_published"}}
<div>
<a href={{model.publishedPage.url}} target="_blank" rel="noopener noreferrer">{{model.publishedPage.url}}</a>

View File

@ -6,17 +6,18 @@ import { h } from "virtual-dom";
import { userPath } from "discourse/lib/url";
import hbs from "discourse/widgets/hbs-compiler";
export function avatarAtts(user) {
export function smallUserAtts(user) {
return {
template: user.avatar_template,
username: user.username,
post_url: user.post_url,
url: userPath(user.username_lower)
url: userPath(user.username_lower),
unknown: user.unknown
};
}
createWidget("small-user-list", {
tagName: "div.clearfix",
tagName: "div.clearfix.small-user-list",
buildClasses(atts) {
return atts.listClassName;
@ -30,7 +31,7 @@ createWidget("small-user-list", {
atts.addSelf &&
!users.some(u => u.username === currentUser.username)
) {
users = users.concat(avatarAtts(currentUser));
users = users.concat(smallUserAtts(currentUser));
}
let description = null;
@ -43,7 +44,13 @@ createWidget("small-user-list", {
let postUrl;
const icons = users.map(u => {
postUrl = postUrl || u.post_url;
return avatarFor.call(this, "small", u);
if (u.unknown) {
return h("div.unknown", {
attributes: { title: I18n.t("post.unknown_user") }
});
} else {
return avatarFor.call(this, "small", u);
}
});
if (postUrl) {

View File

@ -12,8 +12,13 @@ export default createWidget("emoji", {
tagName: "img.emoji",
buildAttributes(attrs) {
let result = { src: emojiUrlFor(attrs.name), alt: `:${attrs.name}:` };
if (attrs.title) result.title = attrs.name;
let result = {
src: emojiUrlFor(attrs.name),
alt: `:${attrs.alt || attrs.name}:`
};
if (attrs.title) {
result.title = typeof attrs.title === "string" ? attrs.title : attrs.name;
}
return result;
}
});

View File

@ -6,6 +6,7 @@ import { h } from "virtual-dom";
import DiscourseURL from "discourse/lib/url";
import { ajax } from "discourse/lib/ajax";
import { userPath } from "discourse/lib/url";
import { wantsNewWindow } from "discourse/lib/intercept-click";
import { NotificationLevels } from "discourse/lib/notification-levels";
const flatten = array => [].concat.apply([], array);
@ -26,14 +27,24 @@ createWidget("priority-faq-link", {
},
click(e) {
e.preventDefault();
if (this.siteSettings.faq_url === this.attrs.href) {
ajax(userPath("read-faq"), { type: "POST" }).then(() => {
this.currentUser.set("read_faq", true);
DiscourseURL.routeToTag($(e.target).closest("a")[0]);
if (wantsNewWindow(e)) {
return;
}
e.preventDefault();
DiscourseURL.routeTo(this.attrs.href);
});
} else {
DiscourseURL.routeToTag($(e.target).closest("a")[0]);
if (wantsNewWindow(e)) {
return;
}
e.preventDefault();
DiscourseURL.routeTo(this.attrs.href);
}
}
});
@ -267,12 +278,7 @@ export default createWidget("hamburger-menu", {
panelContents() {
const { currentUser } = this;
const results = [];
let faqUrl = this.siteSettings.faq_url;
if (!faqUrl || faqUrl.length === 0) {
faqUrl = getURL("/faq");
}
const faqUrl = this.siteSettings.faq_url || getURL("/faq");
const prioritizeFaq =
this.settings.showFAQ && this.currentUser && !this.currentUser.read_faq;

View File

@ -14,6 +14,7 @@ createWidget("post-admin-menu-button", {
return this.attach("button", {
className: attrs.className,
action: attrs.action,
url: attrs.url,
icon: attrs.icon,
label: attrs.label,
secondaryAction: attrs.secondaryAction
@ -30,7 +31,7 @@ export function buildManageButtons(attrs, currentUser, siteSettings) {
if (currentUser.staff) {
contents.push({
icon: "list",
className: "btn-default",
className: "popup-menu-button moderation-history",
label: "review.moderation_history",
url: `/review?topic_id=${attrs.topicId}&status=all`
});

View File

@ -1,6 +1,6 @@
import { next, run } from "@ember/runloop";
import { applyDecorators, createWidget } from "discourse/widgets/widget";
import { avatarAtts } from "discourse/widgets/actions-summary";
import { smallUserAtts } from "discourse/widgets/actions-summary";
import { h } from "virtual-dom";
import showModal from "discourse/lib/show-modal";
import { Promise } from "rsvp";
@ -696,7 +696,7 @@ export default createWidget("post-menu", {
post_action_type_id: LIKE_ACTION
})
.then(users => {
state.likedUsers = users.map(avatarAtts);
state.likedUsers = users.map(smallUserAtts);
state.total = users.totalRows;
});
},
@ -705,7 +705,7 @@ export default createWidget("post-menu", {
const { attrs, state } = this;
return this.store.find("post-reader", { id: attrs.id }).then(users => {
state.readers = users.map(avatarAtts);
state.readers = users.map(smallUserAtts);
state.totalReaders = users.totalRows;
});
},

View File

@ -147,6 +147,7 @@ function videoHTML(token, opts) {
const preloadType = opts.secureMedia ? "none" : "metadata";
const dataOrigSrcAttr = origSrc !== null ? `data-orig-src="${origSrc}"` : "";
return `<div class="video-container">
<p class="video-description">${opts.alt}</p>
<video width="100%" height="100%" preload="${preloadType}" controls>
<source src="${src}" ${dataOrigSrcAttr}>
<a href="${src}">${src}</a>
@ -176,7 +177,8 @@ function renderImageOrPlayableMedia(tokens, idx, options, env, slf) {
// see https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
// handles |video and |audio alt transformations for image tags
const mediaOpts = {
secureMedia: options.discourse.limitedSiteSettings.secureMedia
secureMedia: options.discourse.limitedSiteSettings.secureMedia,
alt: split[0]
};
if (split[1] === "video") {
return videoHTML(token, mediaOpts);

View File

@ -185,6 +185,7 @@ export const DEFAULT_LIST = [
"span.excerpt",
"div.excerpt",
"div.video-container",
"p.video-description",
"span.hashtag",
"span.mention",
"strike",

View File

@ -18,6 +18,13 @@ function buildTimeframe(opts) {
}
export const TIMEFRAMES = [
buildTimeframe({
id: "now",
format: "h:mm a",
enabled: opts => opts.canScheduleNow,
when: time => time.add(1, "minute"),
icon: "magic"
}),
buildTimeframe({
id: "later_today",
format: "h a",
@ -214,6 +221,7 @@ export default ComboBoxComponent.extend(DatetimeMixin, {
includeFarFuture: this.includeFarFuture,
includeDateTime: this.includeDateTime,
includeBasedOnLastPost: this.statusType === CLOSE_STATUS_TYPE,
canScheduleNow: this.includeNow || false,
canScheduleToday: 24 - now.hour() > 6
};

View File

@ -1,8 +1,8 @@
import MultiSelectComponent from "select-kit/components/multi-select";
export default MultiSelectComponent.extend({
pluginApiIdentifiers: ["admin-group-selector"],
classNames: ["admin-group-selector"],
pluginApiIdentifiers: ["group-chooser"],
classNames: ["group-chooser"],
selectKitOptions: {
allowAny: false
}

View File

@ -45,7 +45,7 @@ export default ComboBox.extend(TagsMixin, {
},
modifyComponentForRow(collection, item) {
if (this.getValue(item) === this.selectKit.filter) {
if (this.getValue(item) === this.selectKit.filter && !item.count) {
return "select-kit/select-kit-row";
}

View File

@ -800,15 +800,19 @@ export default Component.extend(
name: "applySmallScreenMaxWidth",
enabled: window.innerWidth <= 450,
phase: "beforeWrite",
fn({ state }) {
fn: ({ state }) => {
if (inModal) {
const innerModal = document.querySelector(
"#discourse-modal div.modal-inner-container"
);
if (innerModal) {
state.styles.popper.width = `${innerModal.clientWidth -
20}px`;
if (this.multiSelect) {
state.styles.popper.width = `${this.element.offsetWidth}px`;
} else {
state.styles.popper.width = `${innerModal.clientWidth -
20}px`;
}
}
} else {
state.styles.popper.width = `${window.innerWidth - 20}px`;

View File

@ -1,4 +1,4 @@
{{#unless isHidden}}
{{#unless selectKit.isHidden}}
{{component selectKit.options.headerComponent
tabindex=tabindex
value=value

View File

@ -1,4 +1,4 @@
{{#unless isHidden}}
{{#unless selectKit.isHidden}}
{{component selectKit.options.headerComponent
tabindex=tabindex
value=value

View File

@ -112,13 +112,13 @@
border-top: 1px solid $primary-low;
}
.buttons {
float: left;
width: 200px;
display: flex;
align-items: center;
button {
margin-right: 0.5em;
}
.saving {
padding: 5px 0 0 0;
margin-left: 10px;
width: 80px;
color: $primary;
order: 3;
}
}
}

View File

@ -623,7 +623,7 @@
}
.select-kit {
width: 150px;
width: 200px;
}
input {
margin: 5px 0;

View File

@ -10,6 +10,7 @@ img.emoji.only-emoji {
margin: 0.5em 0;
}
a,
.md-table,
.poll {
img.emoji.only-emoji {

View File

@ -689,24 +689,51 @@
}
}
.publish-page-modal .modal-body {
p.publish-description {
margin-top: 0;
}
input.publish-slug {
width: 100%;
}
.publish-page-modal {
.modal-body {
p.publish-description {
margin-top: 0;
}
input.publish-slug {
width: 100%;
}
.publish-url {
margin-bottom: 1em;
.example-url,
.invalid-slug {
font-weight: bold;
.publish-url {
margin-bottom: 1em;
.example-url,
.invalid-slug {
font-weight: bold;
}
}
.publish-slug:disabled {
cursor: not-allowed;
}
.controls {
margin-bottom: 1em;
.description {
margin: 0;
display: flex;
align-items: center;
}
}
}
.publish-slug:disabled {
cursor: not-allowed;
.modal-footer {
display: flex;
.close-publish-page {
margin-left: auto;
margin-right: 0;
}
}
}
.ignore-duration-with-username-modal {
.future-date-input {
margin-top: 1em;
}
}

View File

@ -703,6 +703,9 @@ aside.onebox.stackexchange .onebox-body {
// Force oneboxed videos to 16:9 aspect ratio
.onebox.video-onebox,
.video-container {
background: $primary-very-low;
border: 1px solid $primary-low;
border-radius: 2px;
position: relative;
padding: 0 0 56.25% 0;
width: 100%;
@ -714,6 +717,12 @@ aside.onebox.stackexchange .onebox-body {
}
}
.video-description {
color: $primary-medium;
margin: 1rem;
position: absolute;
}
.onebox-placeholder-container {
position: relative;
width: 100%;

View File

@ -264,6 +264,16 @@ blockquote {
}
}
.small-user-list .unknown {
display: inline-block;
background-color: $primary-low;
width: 25px;
height: 25px;
border-radius: 50%;
vertical-align: middle;
margin-right: 0.25em;
}
.post-hidden {
.topic-avatar,
.cooked,

View File

@ -305,4 +305,13 @@ a.topic-featured-link {
2}
);
align-items: center;
.is-public {
padding: 0.25em 0.5em;
font-size: $font-down-2;
background: $tertiary;
color: $secondary;
border-radius: 3px;
text-transform: lowercase;
}
}

View File

@ -11,7 +11,7 @@
.ignored-user-list-item {
border: 1px solid $primary-medium;
border-radius: 0.25em;
border-radius: 5px;
padding: 0;
display: flex;
align-items: center;

View File

@ -27,6 +27,7 @@
.d-icon {
flex: 0 0 100%;
overflow: hidden;
font-size: $font-up-2;
align-self: center;
margin-right: 0;

View File

@ -7,10 +7,6 @@
vertical-align: middle;
user-select: none;
&.is-hidden {
display: none !important;
}
&.is-disabled {
pointer-events: none;
}
@ -71,6 +67,7 @@
.selected-name {
text-align: left;
flex: 0 1 auto;
align-items: center;
color: inherit;
display: flex;
outline: none;

View File

@ -22,11 +22,14 @@ $avatar_width: 120px;
margin-top: 1em;
max-width: 100%;
li {
flex: 1;
flex: 1 0 auto;
min-width: 0;
&:nth-child(2) {
border-left: 0.5em solid transparent;
}
&:empty {
display: none;
}
button {
@include ellipsis;
}

View File

@ -86,7 +86,3 @@
#main-outlet {
padding-top: 4.2857em;
}
.search-link .badge-category {
display: none;
}

View File

@ -116,6 +116,10 @@
max-height: 90vh;
overflow: auto;
}
&.insert-hyperlink-modal .modal-inner-container {
overflow: visible;
}
}
.modal .modal-body.reorder-categories {

View File

@ -61,7 +61,7 @@ class Admin::SiteSettingsController < Admin::AdminController
(new_category_ids - previous_category_ids).each do |category_id|
skip_user_ids = CategoryUser.where(category_id: category_id).pluck(:user_id)
User.where.not(id: skip_user_ids).select(:id).find_in_batches do |users|
User.real.where(staged: false).where.not(id: skip_user_ids).select(:id).find_in_batches do |users|
category_users = []
users.each { |user| category_users << { category_id: category_id, user_id: user.id, notification_level: notification_level } }
CategoryUser.insert_all!(category_users)
@ -88,7 +88,7 @@ class Admin::SiteSettingsController < Admin::AdminController
(new_tag_ids - previous_tag_ids).each do |tag_id|
skip_user_ids = TagUser.where(tag_id: tag_id).pluck(:user_id)
User.where.not(id: skip_user_ids).select(:id).find_in_batches do |users|
User.real.where(staged: false).where.not(id: skip_user_ids).select(:id).find_in_batches do |users|
tag_users = []
users.each { |user| tag_users << { tag_id: tag_id, user_id: user.id, notification_level: notification_level, created_at: now, updated_at: now } }
TagUser.insert_all!(tag_users)
@ -135,8 +135,10 @@ class Admin::SiteSettingsController < Admin::AdminController
user_ids = CategoryUser.where(category_id: previous_category_ids - new_category_ids, notification_level: notification_level).distinct.pluck(:user_id)
user_ids += User
.real
.joins("CROSS JOIN categories c")
.joins("LEFT JOIN category_users cu ON users.id = cu.user_id AND c.id = cu.category_id")
.where(staged: false)
.where("c.id IN (?) AND cu.notification_level IS NULL", new_category_ids - previous_category_ids)
.distinct
.pluck("users.id")
@ -159,8 +161,10 @@ class Admin::SiteSettingsController < Admin::AdminController
user_ids = TagUser.where(tag_id: previous_tag_ids - new_tag_ids, notification_level: notification_level).distinct.pluck(:user_id)
user_ids += User
.real
.joins("CROSS JOIN tags t")
.joins("LEFT JOIN tag_users tu ON users.id = tu.user_id AND t.id = tu.tag_id")
.where(staged: false)
.where("t.id IN (?) AND tu.notification_level IS NULL", new_tag_ids - previous_tag_ids)
.distinct
.pluck("users.id")

View File

@ -539,6 +539,7 @@ class ApplicationController < ActionController::Base
store_preloaded("customHTML", custom_html_json)
store_preloaded("banner", banner_json)
store_preloaded("customEmoji", custom_emoji)
store_preloaded("isReadOnly", @readonly_mode.to_s)
end
def preload_current_user_data

View File

@ -9,7 +9,7 @@ class CategoryHashtagsController < ApplicationController
ids = category_slugs.map { |category_slug| Category.query_from_hashtag_slug(category_slug).try(:id) }
valid_categories = Category.secured(guardian).where(id: ids).map do |category|
{ slug: category.hashtag_slug, url: category.url_with_id }
{ slug: category.hashtag_slug, url: category.url }
end.compact
render json: { valid: valid_categories }

View File

@ -144,6 +144,10 @@ class ListController < ApplicationController
def self.generate_message_route(action)
define_method("#{action}") do
if action == :private_messages_tag && !guardian.can_tag_pms?
raise Discourse::NotFound
end
list_opts = build_topic_list_options
target_user = fetch_user_from_params({ include_inactive: current_user.try(:staff?) }, [:user_stat, :user_option])
guardian.ensure_can_see_private_messages!(target_user.id)

View File

@ -15,6 +15,16 @@ class PostActionUsersController < ApplicationController
post = finder.first
guardian.ensure_can_see!(post)
unknown_user_ids = Set.new
if current_user.present?
result = DB.query_single(<<~SQL, user_id: current_user.id)
SELECT mu.muted_user_id AS id FROM muted_users AS mu WHERE mu.user_id = :user_id
UNION
SELECT iu.ignored_user_id AS id FROM ignored_users AS iu WHERE iu.user_id = :user_id
SQL
unknown_user_ids.merge(result)
end
post_actions = post.post_actions.where(post_action_type_id: post_action_type_id)
.includes(:user)
.offset(page * page_size)
@ -29,7 +39,13 @@ class PostActionUsersController < ApplicationController
action_type = PostActionType.types.key(post_action_type_id)
total_count = post["#{action_type}_count"].to_i
data = { post_action_users: serialize_data(post_actions.to_a, PostActionUserSerializer) }
data = {
post_action_users: serialize_data(
post_actions.to_a,
PostActionUserSerializer,
unknown_user_ids: unknown_user_ids
)
}
if total_count > page_size
data[:total_rows_post_action_users] = total_count

View File

@ -5,6 +5,7 @@ class PublishedPagesController < ApplicationController
skip_before_action :preload_json
skip_before_action :check_xhr, :verify_authenticity_token, only: [:show]
before_action :ensure_publish_enabled
before_action :redirect_to_login_if_required, except: [:show]
def show
params.require(:slug)
@ -12,7 +13,22 @@ class PublishedPagesController < ApplicationController
pp = PublishedPage.find_by(slug: params[:slug])
raise Discourse::NotFound unless pp
guardian.ensure_can_see!(pp.topic)
return if enforce_login_required!
if !pp.public
begin
guardian.ensure_can_see!(pp.topic)
rescue Discourse::InvalidAccess => e
return rescue_discourse_actions(
:invalid_access,
403,
include_ember: false,
custom_message: e.custom_message,
group: e.group
)
end
end
@topic = pp.topic
@canonical_url = @topic.url
@ -37,7 +53,15 @@ class PublishedPagesController < ApplicationController
end
def upsert
result, pp = PublishedPage.publish!(current_user, fetch_topic, params[:published_page][:slug].strip)
pp_params = params.require(:published_page)
result, pp = PublishedPage.publish!(
current_user,
fetch_topic,
pp_params[:slug].strip,
pp_params.permit(:public)
)
json_result(pp, serializer: PublishedPageSerializer) { result }
end
@ -68,4 +92,13 @@ private
raise Discourse::NotFound unless SiteSetting.enable_page_publishing?
end
def enforce_login_required!
if SiteSetting.login_required? &&
!current_user &&
!SiteSetting.show_published_pages_login_required? &&
redirect_to_login
true
end
end
end

View File

@ -37,10 +37,10 @@ class TagsController < ::ApplicationController
ungrouped_tags = ungrouped_tags.where("tags.topic_count > 0") unless show_all_tags
grouped_tag_counts = TagGroup.visible(guardian).order('name ASC').includes(:tags).map do |tag_group|
{ id: tag_group.id, name: tag_group.name, tags: self.class.tag_counts_json(tag_group.tags.where(target_tag_id: nil)) }
{ id: tag_group.id, name: tag_group.name, tags: self.class.tag_counts_json(tag_group.tags.where(target_tag_id: nil), show_pm_tags: guardian.can_tag_pms?) }
end
@tags = self.class.tag_counts_json(ungrouped_tags)
@tags = self.class.tag_counts_json(ungrouped_tags, show_pm_tags: guardian.can_tag_pms?)
@extras = { tag_groups: grouped_tag_counts }
else
tags = show_all_tags ? Tag.all : Tag.where("tags.topic_count > 0")
@ -54,7 +54,7 @@ class TagsController < ::ApplicationController
{ id: c.id, tags: self.class.tag_counts_json(c.tags.where(target_tag_id: nil)) }
end
@tags = self.class.tag_counts_json(unrestricted_tags)
@tags = self.class.tag_counts_json(unrestricted_tags, show_pm_tags: guardian.can_tag_pms?)
@extras = { categories: category_tag_counts }
end
@ -231,7 +231,7 @@ class TagsController < ::ApplicationController
filter_params
)
tags = self.class.tag_counts_json(tags_with_counts)
tags = self.class.tag_counts_json(tags_with_counts, show_pm_tags: guardian.can_tag_pms?)
json_response = { results: tags }
@ -336,17 +336,19 @@ class TagsController < ::ApplicationController
raise Discourse::NotFound if DiscourseTagging.hidden_tag_names(guardian).include?(params[:tag_id])
end
def self.tag_counts_json(tags)
def self.tag_counts_json(tags, show_pm_tags: true)
target_tags = Tag.where(id: tags.map(&:target_tag_id).compact.uniq).select(:id, :name)
tags.map do |t|
next if t.topic_count == 0 && t.pm_topic_count > 0 && !show_pm_tags
{
id: t.name,
text: t.name,
count: t.topic_count,
pm_count: t.pm_topic_count,
pm_count: show_pm_tags ? t.pm_topic_count : 0,
target_tag: t.target_tag_id ? target_tags.find { |x| x.id == t.target_tag_id }&.name : nil
}
end
end.compact
end
def set_category_from_params

View File

@ -195,10 +195,12 @@ class UsersController < ApplicationController
end
email, *secondary_emails = user.emails
unconfirmed_emails = user.unconfirmed_emails
render json: {
email: email,
secondary_emails: secondary_emails,
unconfirmed_emails: unconfirmed_emails,
associated_accounts: user.associated_accounts
}
rescue Discourse::InvalidAccess
@ -232,7 +234,7 @@ class UsersController < ApplicationController
if current_user.staff? && current_user != user
StaffActionLogger.new(current_user).log_update_email(user)
else
UserHistory.create!(action: UserHistory.actions[:update_email], target_user_id: user.id)
UserHistory.create!(action: UserHistory.actions[:update_email], acting_user_id: user.id)
end
end
@ -264,7 +266,7 @@ class UsersController < ApplicationController
if current_user.staff? && current_user != user
StaffActionLogger.new(current_user).log_destroy_email(user)
else
UserHistory.create(action: UserHistory.actions[:destroy_email], target_user_id: user.id)
UserHistory.create(action: UserHistory.actions[:destroy_email], acting_user_id: user.id)
end
end
@ -1176,6 +1178,7 @@ class UsersController < ApplicationController
user = fetch_user_from_params
if params[:notification_level] == "ignore"
@error_message = "ignore_error"
guardian.ensure_can_ignore_user!(user)
MutedUser.where(user: current_user, muted_user: user).delete_all
ignored_user = IgnoredUser.find_by(user: current_user, ignored_user: user)
@ -1185,6 +1188,7 @@ class UsersController < ApplicationController
IgnoredUser.create!(user: current_user, ignored_user: user, expiring_at: Time.parse(params[:expiring_at]))
end
elsif params[:notification_level] == "mute"
@error_message = "mute_error"
guardian.ensure_can_mute_user!(user)
IgnoredUser.where(user: current_user, ignored_user: user).delete_all
MutedUser.find_or_create_by!(user: current_user, muted_user: user)
@ -1194,6 +1198,8 @@ class UsersController < ApplicationController
end
render json: success_json
rescue Discourse::InvalidAccess => e
render_json_error(I18n.t("notification_level.#{@error_message}"))
end
def read_faq

View File

@ -1,92 +0,0 @@
# frozen_string_literal: true
module Jobs
class MigrateUrlSiteSettings < ::Jobs::Onceoff
SETTINGS = [
['logo_url', 'logo'],
['logo_small_url', 'logo_small'],
['digest_logo_url', 'digest_logo'],
['mobile_logo_url', 'mobile_logo'],
['large_icon_url', 'large_icon'],
['favicon_url', 'favicon'],
['apple_touch_icon_url', 'apple_touch_icon'],
['default_opengraph_image_url', 'opengraph_image'],
['twitter_summary_large_image_url', 'twitter_summary_large_image'],
['push_notifications_icon_url', 'push_notifications_icon'],
]
def execute_onceoff(args)
SETTINGS.each do |old_setting, new_setting|
upload = SiteSetting.get(new_setting)
next if upload && upload.id >= Upload::SEEDED_ID_THRESHOLD
old_url = DB.query_single(
"SELECT value FROM site_settings WHERE name = '#{old_setting}'"
).first
next if old_url.blank?
count = 0
file = nil
sleep_interval = 5
loop do
url = UrlHelper.absolute_without_cdn(old_url)
begin
file = FileHelper.download(
url,
max_file_size: [
SiteSetting.max_image_size_kb.kilobytes,
20.megabytes
].max,
tmp_file_name: 'tmp_site_setting_logo',
skip_rate_limit: true,
follow_redirect: true
)
rescue OpenURI::HTTPError,
OpenSSL::SSL::SSLError,
Net::OpenTimeout,
Net::ReadTimeout,
Errno::ECONNREFUSED,
EOFError,
SocketError,
Discourse::InvalidParameters => e
logger.warn(
"Error encountered when trying to download file " +
"for #{new_setting}.\n#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
)
end
count += 1
break if file || (file.blank? && count >= 3)
logger.info(
"Failed to download upload from #{url} for #{new_setting}. Retrying..."
)
sleep(count * sleep_interval)
end
next if file.blank?
upload = UploadCreator.new(
file,
"#{new_setting}",
origin: UrlHelper.absolute(old_url),
for_site_setting: true
).create_for(Discourse.system_user.id)
SiteSetting.set(new_setting, upload)
end
end
private
def logger
Rails.logger
end
end
end

View File

@ -41,7 +41,7 @@ module Jobs
end
if !post.user&.staff? && !post.user&.staged?
s = post.cooked
s = post.raw
s << " #{post.topic.title}" if post.post_number == 1
if !args[:bypass_bump] && WordWatcher.new(s).should_flag?
PostActionCreator.create(

View File

@ -171,10 +171,13 @@ module Jobs
# make sure we actually have a url
return false unless src.present?
# If file is on the forum or CDN domain or already has the
# secure media url
if Discourse.store.has_been_uploaded?(src) || src =~ /\A\/[^\/]/i || Upload.secure_media_url?(src)
return false if src =~ /\/images\/emoji\//
local_bases = [
Discourse.base_url,
Discourse.asset_host,
].compact.map { |s| normalize_src(s) }
if Discourse.store.has_been_uploaded?(src) || normalize_src(src).start_with?(*local_bases) || src =~ /\A\/[^\/]/i
return false if !(src =~ /\/uploads\// || Upload.secure_media_url?(src))
# Someone could hotlink a file from a different site on the same CDN,
# so check whether we have it in this database
@ -220,7 +223,7 @@ module Jobs
uri.normalize!
uri.scheme = nil
uri.to_s
rescue URI::Error
rescue URI::Error, Addressable::URI::InvalidURIError
src
end
end

View File

@ -1,16 +0,0 @@
# frozen_string_literal: true
module Jobs
class CleanUpDeprecatedUrlSiteSettings < ::Jobs::Scheduled
every 1.day
def execute(args)
::Jobs::MigrateUrlSiteSettings::SETTINGS.each do |old_setting, new_setting|
if SiteSetting.where("name = ? AND value IS NOT NULL", new_setting).exists?
SiteSetting.set(old_setting, nil, warn: false)
SiteSetting.find_by(name: old_setting).destroy!
end
end
end
end
end

View File

@ -27,15 +27,6 @@ module Jobs
# Any URLs in site settings are fair game
ignore_urls = [
SiteSetting.logo_url(warn: false),
SiteSetting.logo_small_url(warn: false),
SiteSetting.digest_logo_url(warn: false),
SiteSetting.mobile_logo_url(warn: false),
SiteSetting.large_icon_url(warn: false),
SiteSetting.favicon_url(warn: false),
SiteSetting.default_opengraph_image_url(warn: false),
SiteSetting.twitter_summary_large_image_url(warn: false),
SiteSetting.apple_touch_icon_url(warn: false),
*SiteSetting.selectable_avatars.split("\n"),
].flatten.map do |url|
if url.present?

View File

@ -8,6 +8,8 @@ module Jobs
CLEANUP_GRACE_PERIOD ||= 1.day.ago
def execute(args)
@verbose = true if args && Hash === args && args[:verbose]
rebuild_problem_topics
rebuild_problem_posts
rebuild_problem_categories
@ -15,11 +17,17 @@ module Jobs
rebuild_problem_tags
clean_post_search_data
clean_topic_search_data
@verbose = nil
end
def rebuild_problem_categories(limit: 500)
category_ids = load_problem_category_ids(limit)
if @verbose
puts "rebuilding #{category_ids.length} categories"
end
category_ids.each do |id|
category = Category.find_by(id: id)
SearchIndexer.index(category, force: true) if category
@ -29,6 +37,10 @@ module Jobs
def rebuild_problem_users(limit: 10000)
user_ids = load_problem_user_ids(limit)
if @verbose
puts "rebuilding #{user_ids.length} users"
end
user_ids.each do |id|
user = User.find_by(id: id)
SearchIndexer.index(user, force: true) if user
@ -38,19 +50,34 @@ module Jobs
def rebuild_problem_topics(limit: 10000)
topic_ids = load_problem_topic_ids(limit)
if @verbose
puts "rebuilding #{topic_ids.length} topics"
end
topic_ids.each do |id|
topic = Topic.find_by(id: id)
SearchIndexer.index(topic, force: true) if topic
end
end
def rebuild_problem_posts(limit: 20000, indexer: SearchIndexer)
def rebuild_problem_posts(limit: 20000, indexer: SearchIndexer, verbose: false)
post_ids = load_problem_post_ids(limit)
verbose ||= @verbose
if verbose
puts "rebuilding #{post_ids.length} posts"
end
i = 0
post_ids.each do |id|
# could be deleted while iterating through batch
if post = Post.find_by(id: id)
indexer.index(post, force: true)
i += 1
if verbose && i % 1000 == 0
puts "#{i} posts reindexed"
end
end
end
end
@ -58,6 +85,10 @@ module Jobs
def rebuild_problem_tags(limit: 10000)
tag_ids = load_problem_tag_ids(limit)
if @verbose
puts "rebuilding #{tag_ids.length} tags"
end
tag_ids.each do |id|
tag = Tag.find_by(id: id)
SearchIndexer.index(tag, force: true) if tag
@ -67,6 +98,8 @@ module Jobs
private
def clean_post_search_data
puts "cleaning up post search data" if @verbose
PostSearchData
.joins("LEFT JOIN posts p ON p.id = post_search_data.post_id")
.where("p.raw = ''")
@ -90,6 +123,8 @@ module Jobs
end
def clean_topic_search_data
puts "cleaning up topic search data" if @verbose
DB.exec(<<~SQL, deleted_at: CLEANUP_GRACE_PERIOD)
DELETE FROM topic_search_data
WHERE topic_id IN (

View File

@ -9,6 +9,6 @@ class SubscriptionMailer < ActionMailer::Base
template: "unsubscribe_mailer",
site_title: SiteSetting.title,
site_domain_name: Discourse.current_hostname,
confirm_unsubscribe_link: "#{Discourse.base_url}/unsubscribe/#{unsubscribe_key}"
confirm_unsubscribe_link: email_unsubscribe_url(unsubscribe_key, host: Discourse.base_url)
end
end

View File

@ -174,14 +174,6 @@ class UserNotifications < ActionMailer::Base
)
end
def short_date(dt)
if dt.year == Time.now.year
I18n.l(dt, format: :short_no_year)
else
I18n.l(dt, format: :date_only)
end
end
def digest(user, opts = {})
build_summary_for(user)
min_date = opts[:since] || user.last_emailed_at || user.last_seen_at || 1.month.ago

View File

@ -721,11 +721,13 @@ class Category < ActiveRecord::Base
end
def url
@@url_cache[self.id] ||= "#{Discourse.base_uri}/c/#{slug_path.join('/')}"
@@url_cache[self.id] ||= "#{Discourse.base_uri}/c/#{slug_path.join('/')}/#{self.id}"
end
def url_with_id
self.parent_category ? "#{url}/#{self.id}" : "#{Discourse.base_uri}/c/#{self.slug}/#{self.id}"
Discourse.deprecate("Category#url_with_id is deprecated. Use `Category#url` instead.", output_in_test: true)
url
end
# If the name changes, try and update the category definition topic too if it's an exact match
@ -739,9 +741,10 @@ class Category < ActiveRecord::Base
def create_category_permalink
old_slug = saved_changes.transform_values(&:first)["slug"]
url = +"#{Discourse.base_uri}/c"
url << "/#{parent_category.slug_path.join('/')}" if parent_category_id
url << "/#{old_slug}"
url << "/#{old_slug}/#{id}"
url = Permalink.normalize_url(url)
if Permalink.where(url: url).exists?

View File

@ -59,7 +59,7 @@ class CategoryFeaturedTopic < ActiveRecord::Base
# no featured topics (all the previous 2x topics are only visible to admins)
# Add topics, even if they're in secured categories or invisible
query = TopicQuery.new(CategoryFeaturedTopic.fake_admin, query_opts)
query = TopicQuery.new(Discourse.system_user, query_opts)
results = query.list_category_topic_ids(c).uniq
# Add some topics that are visible to everyone:
@ -81,15 +81,6 @@ class CategoryFeaturedTopic < ActiveRecord::Base
end
end
end
def self.fake_admin
# fake an admin
admin = User.new
admin.admin = true
admin.id = -1
admin
end
end
# == Schema Information

View File

@ -19,6 +19,7 @@ module Roleable
end
def grant_moderation!
return if moderator
set_permission('moderator', true)
auto_approve_user
enqueue_staff_welcome_message(:moderator)
@ -29,6 +30,7 @@ module Roleable
end
def grant_admin!
return if admin
set_permission('admin', true)
auto_approve_user
enqueue_staff_welcome_message(:admin)

View File

@ -6,8 +6,10 @@ class EmailLog < ActiveRecord::Base
admin_login
confirm_new_email
confirm_old_email
confirm_old_email_add
forgot_password
notify_old_email
notify_old_email_add
signup
signup_after_approval
}

View File

@ -61,7 +61,7 @@ class EmbeddableHost < ActiveRecord::Base
end
def host_must_be_valid
if host !~ /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,24}(:[0-9]{1,5})?(\/.*)?\Z/i &&
if host !~ /\A[a-z0-9]+([\-\.]+{1}[a-z0-9]+)*\.[a-z]{2,24}(:[0-9]{1,5})?(\/.*)?\Z/i &&
host !~ /\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(:[0-9]{1,5})?(\/.*)?\Z/ &&
host !~ /\A([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.)?localhost(\:[0-9]{1,5})?(\/.*)?\Z/i
errors.add(:host, I18n.t('errors.messages.invalid'))

View File

@ -134,12 +134,6 @@ class GlobalSetting
end
end
if hash["replica_host"]
if !ENV["ACTIVE_RECORD_RAILS_FAILOVER"]
hash["adapter"] = "postgresql_fallback"
end
end
hostnames = [ hostname ]
hostnames << backup_hostname if backup_hostname.present?
@ -151,6 +145,7 @@ class GlobalSetting
hash["prepared_statements"] = !!self.db_prepared_statements
hash["idle_timeout"] = connection_reaper_age if connection_reaper_age.present?
hash["reaping_frequency"] = connection_reaper_interval if connection_reaper_interval.present?
hash["advisory_locks"] = !!self.db_advisory_locks
{ "production" => hash }
end
@ -168,16 +163,10 @@ class GlobalSetting
c[:host] = redis_host if redis_host
c[:port] = redis_port if redis_port
if redis_slave_host && redis_slave_port
if ENV["REDIS_RAILS_FAILOVER"]
c[:replica_host] = redis_slave_host
c[:replica_port] = redis_slave_port
c[:connector] = RailsFailover::Redis::Connector
else
c[:slave_host] = redis_slave_host
c[:slave_port] = redis_slave_port
c[:connector] = DiscourseRedis::Connector
end
if redis_slave_host && redis_slave_port && defined?(RailsFailover)
c[:replica_host] = redis_slave_host
c[:replica_port] = redis_slave_port
c[:connector] = RailsFailover::Redis::Connector
end
c[:password] = redis_password if redis_password.present?
@ -199,15 +188,9 @@ class GlobalSetting
c[:port] = message_bus_redis_port if message_bus_redis_port
if message_bus_redis_slave_host && message_bus_redis_slave_port
if ENV["REDIS_RAILS_FAILOVER"]
c[:replica_host] = message_bus_redis_slave_host
c[:replica_port] = message_bus_redis_slave_port
c[:connector] = RailsFailover::Redis::Connector
else
c[:slave_host] = message_bus_redis_slave_host
c[:slave_port] = message_bus_redis_slave_port
c[:connector] = DiscourseRedis::Connector
end
c[:replica_host] = message_bus_redis_slave_host
c[:replica_port] = message_bus_redis_slave_port
c[:connector] = RailsFailover::Redis::Connector
end
c[:password] = message_bus_redis_password if message_bus_redis_password.present?

View File

@ -57,7 +57,6 @@ class Group < ActiveRecord::Base
def remove_review_groups
Category.where(review_group_id: self.id).update_all(review_group_id: nil)
Category.where(review_group_id: self.id).update_all(review_group_id: nil)
end
validate :name_format_validator

View File

@ -128,10 +128,14 @@ InviteRedeemer = Struct.new(:invite, :email, :username, :name, :password, :user_
end
def add_user_to_groups
guardian = Guardian.new(invite.invited_by)
new_group_ids = invite.groups.pluck(:id) - invited_user.group_users.pluck(:group_id)
new_group_ids.each do |id|
invited_user.group_users.create!(group_id: id)
DiscourseEvent.trigger(:user_added_to_group, invited_user, Group.find_by(id: id), automatic: false)
group = Group.find_by(id: id)
if guardian.can_edit_group?(group)
invited_user.group_users.create!(group_id: group.id)
DiscourseEvent.trigger(:user_added_to_group, invited_user, group, automatic: false)
end
end
end

View File

@ -80,7 +80,7 @@ class Permalink < ActiveRecord::Base
return external_url if external_url
return "#{Discourse::base_uri}#{post.url}" if post
return topic.relative_url if topic
return "#{category.url}/#{category.id}" if category
return category.url if category
return tag.full_url if tag
nil
end

View File

@ -38,7 +38,7 @@ class Post < ActiveRecord::Base
has_many :topic_links
has_many :group_mentions, dependent: :destroy
has_many :post_uploads
has_many :post_uploads, dependent: :delete_all
has_many :uploads, through: :post_uploads
has_one :post_stat
@ -255,7 +255,7 @@ class Post < ActiveRecord::Base
end
def self.white_listed_image_classes
@white_listed_image_classes ||= ['avatar', 'favicon', 'thumbnail', 'emoji']
@white_listed_image_classes ||= ['avatar', 'favicon', 'thumbnail', 'emoji', 'ytp-thumbnail-image']
end
def post_analyzer
@ -752,18 +752,16 @@ class Post < ActiveRecord::Base
# Enqueue post processing for this post
def trigger_post_process(bypass_bump: false, priority: :normal, new_post: false, skip_pull_hotlinked_images: false)
args = {
post_id: id,
bypass_bump: bypass_bump,
cooking_options: self.cooking_options,
new_post: new_post,
post_id: id,
skip_pull_hotlinked_images: skip_pull_hotlinked_images,
}
args[:image_sizes] = image_sizes if image_sizes.present?
args[:invalidate_oneboxes] = true if invalidate_oneboxes.present?
args[:cooking_options] = self.cooking_options
if priority && priority != :normal
args[:queue] = priority.to_s
end
args[:image_sizes] = image_sizes if self.image_sizes.present?
args[:invalidate_oneboxes] = true if self.invalidate_oneboxes.present?
args[:queue] = priority.to_s if priority && priority != :normal
Jobs.enqueue(:process_post, args)
DiscourseEvent.trigger(:after_trigger_post_process, self)

View File

@ -33,7 +33,10 @@ class PostAnalyzer
result = Oneboxer.apply(cooked) do |url|
@onebox_urls << url
Oneboxer.invalidate(url) if opts[:invalidate_oneboxes]
if opts[:invalidate_oneboxes]
Oneboxer.invalidate(url)
InlineOneboxer.invalidate(url)
end
onebox = Oneboxer.cached_onebox(url)
@found_oneboxes = true if onebox.present?
onebox

View File

@ -23,12 +23,13 @@ class PublishedPage < ActiveRecord::Base
"#{Discourse.base_url}#{path}"
end
def self.publish!(publisher, topic, slug)
def self.publish!(publisher, topic, slug, options = {})
pp = nil
transaction do
pp = find_or_initialize_by(topic: topic)
pp.slug = slug.strip
pp.public = options[:public] || false
if pp.save
StaffActionLogger.new(publisher).log_published_page(topic.id, slug)
@ -56,6 +57,7 @@ end
# slug :string not null
# created_at :datetime not null
# updated_at :datetime not null
# public :boolean default(FALSE), not null
#
# Indexes
#

View File

@ -155,7 +155,12 @@ class Topic < ActiveRecord::Base
message: :has_already_been_used,
allow_blank: true,
case_sensitive: false,
collection: Proc.new { Topic.listable_topics } }
collection: Proc.new { |t|
SiteSetting.allow_duplicate_topic_titles_category? ?
Topic.listable_topics.where("category_id = ?", t.category_id) :
Topic.listable_topics
}
}
validates :category_id,
presence: true,

View File

@ -67,7 +67,7 @@ class TopicList
def preload_key
if @category
"topic_list_#{@category.url.sub(/^\//, '')}/#{@category.id}/l/#{@filter}"
"topic_list_#{@category.url.sub(/^\//, '')}/l/#{@filter}"
else
"topic_list_#{@filter}"
end

View File

@ -24,18 +24,35 @@ class TopicTrackingState
def self.publish_new(topic)
return unless topic.regular?
tags, tag_ids = nil
if SiteSetting.tagging_enabled
topic.tags.pluck(:id, :name).each do |id, name|
tags ||= []
tag_ids ||= []
tags << name
tag_ids << id
end
end
payload = {
last_read_post_number: nil,
highest_post_number: 1,
created_at: topic.created_at,
topic_id: topic.id,
category_id: topic.category_id,
archetype: topic.archetype,
}
if tags
payload[:tags] = tags
payload[:topic_tag_ids] = tag_ids
end
message = {
topic_id: topic.id,
message_type: "new_topic",
payload: {
last_read_post_number: nil,
highest_post_number: 1,
created_at: topic.created_at,
topic_id: topic.id,
category_id: topic.category_id,
archetype: topic.archetype,
topic_tag_ids: topic.tags.pluck(:id)
}
payload: payload
}
group_ids = topic.category && topic.category.secure_group_ids
@ -99,22 +116,31 @@ class TopicTrackingState
post.topic.category && post.topic.category.secure_group_ids
end
tags = nil
if include_tags_in_report?
tags = post.topic.tags.pluck(:name)
end
TopicUser
.tracking(post.topic_id)
.select([:user_id, :last_read_post_number, :notification_level])
.each do |tu|
payload = {
last_read_post_number: tu.last_read_post_number,
highest_post_number: post.post_number,
created_at: post.created_at,
category_id: post.topic.category_id,
notification_level: tu.notification_level,
archetype: post.topic.archetype
}
payload[:tags] = tags if tags
message = {
topic_id: post.topic_id,
message_type: UNREAD_MESSAGE_TYPE,
payload: {
last_read_post_number: tu.last_read_post_number,
highest_post_number: post.post_number,
created_at: post.created_at,
category_id: post.topic.category_id,
notification_level: tu.notification_level,
archetype: post.topic.archetype
}
payload: payload
}
MessageBus.publish(self.unread_channel_key(tu.user_id), message.as_json, group_ids: group_ids)
@ -186,7 +212,7 @@ class TopicTrackingState
end
def self.include_tags_in_report?
@include_tags_in_report
SiteSetting.tagging_enabled && (@include_tags_in_report || SiteSetting.show_filter_by_tag)
end
def self.include_tags_in_report=(v)

View File

@ -329,7 +329,7 @@ class Upload < ActiveRecord::Base
follow_redirect: true
)
rescue OpenURI::HTTPError
retry if (retires += 1) < 1
retry if (retries += 1) < 1
next
end

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