From 4c0aa20daede759b5178111352c426cde3b0ae8d Mon Sep 17 00:00:00 2001
From: Osama Sayegh
Date: Thu, 21 Jan 2021 23:52:24 +0300
Subject: [PATCH 01/85] FIX: Share popup is positioned incorrectly in RTL
locales (#11792)
---
.../discourse/app/components/share-popup.js | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/app/assets/javascripts/discourse/app/components/share-popup.js b/app/assets/javascripts/discourse/app/components/share-popup.js
index 3b2c3eb093..a2d9decc13 100644
--- a/app/assets/javascripts/discourse/app/components/share-popup.js
+++ b/app/assets/javascripts/discourse/app/components/share-popup.js
@@ -56,7 +56,7 @@ export default Component.extend({
},
_showUrl($target, url) {
- const $currentTargetOffset = $target.offset();
+ const currentTargetOffset = $target.offset();
const $this = $(this.element);
if (isEmpty(url)) {
@@ -69,7 +69,7 @@ export default Component.extend({
}
const shareLinkWidth = $this.width();
- let x = $currentTargetOffset.left - shareLinkWidth / 2;
+ let x = currentTargetOffset.left - shareLinkWidth / 2;
if (x < 25) {
x = 25;
}
@@ -78,15 +78,18 @@ export default Component.extend({
}
const header = $(".d-header");
- let y = $currentTargetOffset.top - ($this.height() + 20);
+ let y = currentTargetOffset.top - ($this.height() + 20);
if (y < header.offset().top + header.height()) {
- y = $currentTargetOffset.top + 10;
+ y = currentTargetOffset.top + 10;
}
- $this.css({ top: "" + y + "px" });
+ this.element.style.top = `${y}px`;
if (!this.site.mobileView) {
- $this.css({ left: "" + x + "px" });
+ this.element.style.left = `${x}px`;
+ if (document.documentElement.classList.contains("rtl")) {
+ this.element.style.right = "unset";
+ }
}
this.set("link", url);
this.set("visible", true);
From 83347ac218f708e172944f087c4781acad41efaf Mon Sep 17 00:00:00 2001
From: Robin Ward
Date: Thu, 21 Jan 2021 15:55:39 -0500
Subject: [PATCH 02/85] DEV: Sync up more Ember CLI features (#11790)
This is mostly changes to acceptance tests to allow them to run in both
versions of Ember.
---
.../addon/config/environment.js | 6 +++
.../discourse-common/addon/lib/debounce.js | 9 ++--
app/assets/javascripts/discourse/app/app.js | 3 +-
.../app/initializers/inject-objects.js | 7 ++-
.../app/pre-initializers/map-routes.js | 5 +-
.../components/edit-category-general.hbs | 2 +-
.../tests/acceptance/category-edit-test.js | 2 +-
.../tests/acceptance/composer-test.js | 14 +++---
.../create-account-user-fields-test.js | 2 +-
.../acceptance/enforce-second-factor-test.js | 19 ++++++--
.../discourse/tests/acceptance/group-test.js | 2 +-
.../invite-show-user-fields-test.js | 2 +-
.../plugin-keyboard-shortcut-test.js | 6 +--
.../tests/acceptance/preferences-test.js | 46 +++++++++++--------
.../acceptance/raw-plugin-outlet-test.js | 2 +-
.../tests/acceptance/search-full-test.js | 4 +-
.../tests/acceptance/sign-in-test.js | 4 --
.../tests/acceptance/tag-groups-test.js | 2 +-
.../discourse/tests/acceptance/tags-test.js | 6 +--
.../tests/acceptance/topic-discovery-test.js | 2 +-
.../discourse/tests/acceptance/topic-test.js | 24 ++++++----
.../tests/acceptance/user-card-test.js | 2 +-
.../discourse/tests/acceptance/user-test.js | 8 +++-
.../tests/helpers/create-pretender.js | 9 ++++
.../tests/unit/controllers/bookmark-test.js | 3 +-
.../discourse/tests/unit/localization-test.js | 5 +-
app/assets/javascripts/handlebars-shim.js | 4 ++
27 files changed, 128 insertions(+), 72 deletions(-)
diff --git a/app/assets/javascripts/discourse-common/addon/config/environment.js b/app/assets/javascripts/discourse-common/addon/config/environment.js
index 409b6af066..d2a47b1247 100644
--- a/app/assets/javascripts/discourse-common/addon/config/environment.js
+++ b/app/assets/javascripts/discourse-common/addon/config/environment.js
@@ -14,6 +14,12 @@ export function isTesting() {
return Ember.testing || environment === "testing";
}
+// Generally means "before we migrated to Ember CLI"
+let _isLegacy = Ember.VERSION.startsWith("3.12");
+export function isLegacyEmber() {
+ return _isLegacy;
+}
+
export function isDevelopment() {
return environment === "development";
}
diff --git a/app/assets/javascripts/discourse-common/addon/lib/debounce.js b/app/assets/javascripts/discourse-common/addon/lib/debounce.js
index 541498fda6..5e30864574 100644
--- a/app/assets/javascripts/discourse-common/addon/lib/debounce.js
+++ b/app/assets/javascripts/discourse-common/addon/lib/debounce.js
@@ -1,14 +1,17 @@
-import { debounce, run } from "@ember/runloop";
-import { isTesting } from "discourse-common/config/environment";
+import { debounce, next, run } from "@ember/runloop";
+import { isLegacyEmber, isTesting } from "discourse-common/config/environment";
+
/**
Debounce a Javascript function. This means if it's called many times in a time limit it
should only be executed once (at the end of the limit counted from the last call made).
Original function will be called with the context and arguments from the last call made.
**/
+let testingFunc = isLegacyEmber() ? run : next;
+
export default function () {
if (isTesting()) {
- return run(...arguments);
+ return testingFunc(...arguments);
} else {
return debounce(...arguments);
}
diff --git a/app/assets/javascripts/discourse/app/app.js b/app/assets/javascripts/discourse/app/app.js
index 093dfe5eea..b28effdddb 100644
--- a/app/assets/javascripts/discourse/app/app.js
+++ b/app/assets/javascripts/discourse/app/app.js
@@ -26,7 +26,8 @@ const Discourse = Application.extend({
const init = module.default;
const oldInitialize = init.initialize;
- init.initialize = () => oldInitialize.call(init, this.__container__, this);
+ init.initialize = (app) => oldInitialize.call(init, app.__container__, app);
+
return init;
},
diff --git a/app/assets/javascripts/discourse/app/initializers/inject-objects.js b/app/assets/javascripts/discourse/app/initializers/inject-objects.js
index abcf14a574..533e9134af 100644
--- a/app/assets/javascripts/discourse/app/initializers/inject-objects.js
+++ b/app/assets/javascripts/discourse/app/initializers/inject-objects.js
@@ -1,6 +1,9 @@
-// backwards compatibility for plugins that depend on this initializer
+import { setDefaultOwner } from "discourse-common/lib/get-owner";
export default {
name: "inject-objects",
- initialize() {},
+ initialize(container, app) {
+ // This is required for Ember CLI tests to work
+ setDefaultOwner(app.__container__);
+ },
};
diff --git a/app/assets/javascripts/discourse/app/pre-initializers/map-routes.js b/app/assets/javascripts/discourse/app/pre-initializers/map-routes.js
index 8c4b69dd0f..d064dd3b32 100644
--- a/app/assets/javascripts/discourse/app/pre-initializers/map-routes.js
+++ b/app/assets/javascripts/discourse/app/pre-initializers/map-routes.js
@@ -1,5 +1,5 @@
import Application from "@ember/application";
-import Ember from "ember";
+import { isLegacyEmber } from "discourse-common/config/environment";
import { registerRouter } from "discourse/mapping-router";
let originalBuildInstance;
@@ -12,8 +12,7 @@ export default {
let router = registerRouter(app);
container.registry.register("router:main", router);
- // TODO: Remove this once we've upgraded Ember everywhere
- if (Ember.VERSION.startsWith("3.12")) {
+ if (isLegacyEmber()) {
// HACK to fix: https://github.com/emberjs/ember.js/issues/10310
originalBuildInstance =
originalBuildInstance || Application.prototype.buildInstance;
diff --git a/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs b/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs
index 353f1be130..60f5047be7 100644
--- a/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs
@@ -71,7 +71,7 @@
{{i18n "category.foreground_color"}}:
-
+
# {{text-field value=category.text_color placeholderKey="category.color_placeholder" maxlength="6"}}
{{color-picker colors=foregroundColors value=category.text_color id="edit-text-color"}}
diff --git a/app/assets/javascripts/discourse/tests/acceptance/category-edit-test.js b/app/assets/javascripts/discourse/tests/acceptance/category-edit-test.js
index 3df14fd12e..7393d1dbb5 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/category-edit-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/category-edit-test.js
@@ -27,7 +27,7 @@ acceptance("Category Edit", function (needs) {
await fillIn("input.category-name", "testing");
assert.equal(queryAll(".badge-category").text(), "testing");
- await fillIn("#edit-text-color", "#ff0000");
+ await fillIn(".edit-text-color input", "#ff0000");
await click(".edit-category-topic-template");
await fillIn(".d-editor-input", "this is the new topic template");
diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js
index 1cec7ceb8c..1514688695 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js
@@ -790,7 +790,7 @@ acceptance("Composer", function (needs) {
await click(
queryAll(
".button-wrapper[data-image-index='0'] .scale-btn[data-scale='50']"
- )
+ )[0]
);
assertImageResized(assert, uploads);
@@ -800,7 +800,7 @@ acceptance("Composer", function (needs) {
await click(
queryAll(
".button-wrapper[data-image-index='3'] .scale-btn[data-scale='50']"
- )
+ )[0]
);
assertImageResized(assert, uploads);
@@ -810,7 +810,7 @@ acceptance("Composer", function (needs) {
await click(
queryAll(
".button-wrapper[data-image-index='4'] .scale-btn[data-scale='75']"
- )
+ )[0]
);
assertImageResized(assert, uploads);
@@ -819,7 +819,7 @@ acceptance("Composer", function (needs) {
await click(
queryAll(
".button-wrapper[data-image-index='5'] .scale-btn[data-scale='50']"
- )
+ )[0]
);
assertImageResized(assert, uploads);
@@ -828,7 +828,7 @@ acceptance("Composer", function (needs) {
await click(
queryAll(
".button-wrapper[data-image-index='6'] .scale-btn[data-scale='75']"
- )
+ )[0]
);
assertImageResized(assert, uploads);
@@ -837,7 +837,7 @@ acceptance("Composer", function (needs) {
await click(
queryAll(
".button-wrapper[data-image-index='8'] .scale-btn[data-scale='75']"
- )
+ )[0]
);
assertImageResized(assert, uploads);
@@ -846,7 +846,7 @@ acceptance("Composer", function (needs) {
await click(
queryAll(
".button-wrapper[data-image-index='9'] .scale-btn[data-scale='75']"
- )
+ )[0]
);
assertImageResized(assert, uploads);
diff --git a/app/assets/javascripts/discourse/tests/acceptance/create-account-user-fields-test.js b/app/assets/javascripts/discourse/tests/acceptance/create-account-user-fields-test.js
index 9bbefe9313..c85cb272c5 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/create-account-user-fields-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/create-account-user-fields-test.js
@@ -64,7 +64,7 @@ acceptance("Create Account - User Fields", function (needs) {
await click(".modal-footer .btn-primary");
assert.equal(queryAll("#modal-alert")[0].style.display, "");
- await fillIn(".user-field input[type=text]:first", "Barky");
+ await fillIn(".user-field input[type=text]:nth-of-type(1)", "Barky");
await click(".user-field input[type=checkbox]");
await click(".modal-footer .btn-primary");
diff --git a/app/assets/javascripts/discourse/tests/acceptance/enforce-second-factor-test.js b/app/assets/javascripts/discourse/tests/acceptance/enforce-second-factor-test.js
index eaf9ca4896..86ce29be17 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/enforce-second-factor-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/enforce-second-factor-test.js
@@ -3,9 +3,20 @@ import {
queryAll,
updateCurrentUser,
} from "discourse/tests/helpers/qunit-helpers";
-import { click, visit } from "@ember/test-helpers";
+import { click, settled, visit } from "@ember/test-helpers";
import { test } from "qunit";
+async function catchAbortedTransition() {
+ try {
+ await visit("/u/eviltrout/summary");
+ } catch (e) {
+ if (e.message !== "TransitionAborted") {
+ throw e;
+ }
+ }
+ await settled();
+}
+
acceptance("Enforce Second Factor", function (needs) {
needs.user();
needs.pretender((server, helper) => {
@@ -21,7 +32,7 @@ acceptance("Enforce Second Factor", function (needs) {
await visit("/u/eviltrout/preferences/second-factor");
this.siteSettings.enforce_second_factor = "staff";
- await visit("/u/eviltrout/summary");
+ await catchAbortedTransition();
assert.equal(
queryAll(".control-label").text(),
@@ -45,7 +56,7 @@ acceptance("Enforce Second Factor", function (needs) {
await visit("/u/eviltrout/preferences/second-factor");
this.siteSettings.enforce_second_factor = "all";
- await visit("/u/eviltrout/summary");
+ await catchAbortedTransition();
assert.equal(
queryAll(".control-label").text(),
@@ -70,7 +81,7 @@ acceptance("Enforce Second Factor", function (needs) {
this.siteSettings.enforce_second_factor = "all";
this.siteSettings.allow_anonymous_posting = true;
- await visit("/u/eviltrout/summary");
+ await catchAbortedTransition();
assert.notEqual(
queryAll(".control-label").text(),
diff --git a/app/assets/javascripts/discourse/tests/acceptance/group-test.js b/app/assets/javascripts/discourse/tests/acceptance/group-test.js
index 2e92868be1..6fba32ebd5 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/group-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/group-test.js
@@ -280,7 +280,7 @@ acceptance("Group - Authenticated", function (needs) {
await click(".group-add-members-modal .modal-close");
- const memberDropdown = selectKit(".group-member-dropdown:first");
+ const memberDropdown = selectKit(".group-member-dropdown:nth-of-type(1)");
await memberDropdown.expand();
assert.equal(
diff --git a/app/assets/javascripts/discourse/tests/acceptance/invite-show-user-fields-test.js b/app/assets/javascripts/discourse/tests/acceptance/invite-show-user-fields-test.js
index a03c92e3a4..814e727756 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/invite-show-user-fields-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/invite-show-user-fields-test.js
@@ -59,7 +59,7 @@ acceptance("Accept Invite - User Fields", function (needs) {
"submit is still disabled due to lack of user fields"
);
- await fillIn(".user-field input[type=text]:first", "Barky");
+ await fillIn(".user-field input[type=text]:nth-of-type(1)", "Barky");
assert.ok(
exists(".invites-show .btn-primary:disabled"),
diff --git a/app/assets/javascripts/discourse/tests/acceptance/plugin-keyboard-shortcut-test.js b/app/assets/javascripts/discourse/tests/acceptance/plugin-keyboard-shortcut-test.js
index 4a517fae5b..42bacdd5f2 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/plugin-keyboard-shortcut-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/plugin-keyboard-shortcut-test.js
@@ -1,4 +1,4 @@
-import { triggerKeyEvent, visit } from "@ember/test-helpers";
+import { getApplication, triggerKeyEvent, visit } from "@ember/test-helpers";
import KeyboardShortcutInitializer from "discourse/initializers/keyboard-shortcuts";
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
@@ -9,7 +9,7 @@ import { withPluginApi } from "discourse/lib/plugin-api";
acceptance("Plugin Keyboard Shortcuts - Logged In", function (needs) {
needs.user();
needs.hooks.beforeEach(function () {
- KeyboardShortcutInitializer.initialize(this.container);
+ KeyboardShortcutInitializer.initialize(getApplication());
});
test("a plugin can add a keyboard shortcut", async function (assert) {
withPluginApi("0.8.38", (api) => {
@@ -32,7 +32,7 @@ acceptance("Plugin Keyboard Shortcuts - Logged In", function (needs) {
acceptance("Plugin Keyboard Shortcuts - Anonymous", function (needs) {
needs.hooks.beforeEach(function () {
- KeyboardShortcutInitializer.initialize(this.container);
+ KeyboardShortcutInitializer.initialize(getApplication());
});
test("a plugin can add a keyboard shortcut with an option", async function (assert) {
let spy = sinon.spy(KeyboardShortcuts, "_bindToPath");
diff --git a/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js b/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js
index e22cfa1000..c95e955991 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/preferences-test.js
@@ -95,18 +95,18 @@ acceptance("User Preferences", function (needs) {
queryAll(".saved").remove();
};
- fillIn(".pref-name input[type=text]", "Jon Snow");
+ await fillIn(".pref-name input[type=text]", "Jon Snow");
await savePreferences();
- click(".preferences-nav .nav-profile a");
- fillIn("#edit-location", "Westeros");
+ await click(".preferences-nav .nav-profile a");
+ await fillIn("#edit-location", "Westeros");
await savePreferences();
- click(".preferences-nav .nav-emails a");
- click(".pref-activity-summary input[type=checkbox]");
+ await click(".preferences-nav .nav-emails a");
+ await click(".pref-activity-summary input[type=checkbox]");
await savePreferences();
- click(".preferences-nav .nav-notifications a");
+ await click(".preferences-nav .nav-notifications a");
await selectKit(
".control-group.notifications .combo-box.duration"
).expand();
@@ -115,8 +115,8 @@ acceptance("User Preferences", function (needs) {
).selectRowByValue(1440);
await savePreferences();
- click(".preferences-nav .nav-categories a");
- fillIn(".tracking-controls .category-selector", "faq");
+ await click(".preferences-nav .nav-categories a");
+ await fillIn(".tracking-controls .category-selector input", "faq");
await savePreferences();
assert.ok(
@@ -124,8 +124,8 @@ acceptance("User Preferences", function (needs) {
"tags tab isn't there when tags are disabled"
);
- click(".preferences-nav .nav-interface a");
- click(".control-group.other input[type=checkbox]:first");
+ await click(".preferences-nav .nav-interface a");
+ await click(".control-group.other input[type=checkbox]:nth-of-type(1)");
savePreferences();
assert.ok(
@@ -175,15 +175,19 @@ acceptance("User Preferences", function (needs) {
"it has the connected accounts section"
);
assert.ok(
- queryAll(".pref-associated-accounts table tr:first td:first")
+ queryAll(
+ ".pref-associated-accounts table tr:nth-of-type(1) td:nth-of-type(1)"
+ )
.html()
.indexOf("Facebook") > -1,
"it lists facebook"
);
- await click(".pref-associated-accounts table tr:first td:last button");
+ await click(
+ ".pref-associated-accounts table tr:nth-of-type(1) td:last-child button"
+ );
- queryAll(".pref-associated-accounts table tr:first td:last button")
+ queryAll(".pref-associated-accounts table tr:nth-of-type(1) td:last button")
.html()
.indexOf("Connect") > -1;
});
@@ -329,7 +333,7 @@ acceptance("User Preferences when badges are disabled", function (needs) {
await visit("/u/eviltrout/preferences");
assert.equal(
- queryAll(".auth-tokens > .auth-token:first .auth-token-device")
+ queryAll(".auth-tokens > .auth-token:nth-of-type(1) .auth-token-device")
.text()
.trim(),
"Linux Computer",
@@ -337,7 +341,7 @@ acceptance("User Preferences when badges are disabled", function (needs) {
);
assert.equal(
- queryAll(".pref-auth-tokens > a:first").text().trim(),
+ queryAll(".pref-auth-tokens > a:nth-of-type(1)").text().trim(),
I18n.t("user.auth_tokens.show_all", { count: 3 }),
"it should display two tokens"
);
@@ -346,14 +350,14 @@ acceptance("User Preferences when badges are disabled", function (needs) {
"it should display two tokens"
);
- await click(".pref-auth-tokens > a:first");
+ await click(".pref-auth-tokens > a:nth-of-type(1)");
assert.ok(
queryAll(".pref-auth-tokens .auth-token").length === 3,
"it should display three tokens"
);
- await click(".auth-token-dropdown:first button");
+ await click(".auth-token-dropdown button:nth-of-type(1)");
await click("li[data-value='notYou']");
assert.ok(queryAll(".d-modal:visible").length === 1, "modal should appear");
@@ -392,7 +396,9 @@ acceptance(
"clear button not present"
);
- const selectTopicBtn = queryAll(".feature-topic-on-profile-btn:first");
+ const selectTopicBtn = queryAll(
+ ".feature-topic-on-profile-btn:nth-of-type(1)"
+ )[0];
assert.ok(exists(selectTopicBtn), "feature topic button is present");
await click(selectTopicBtn);
@@ -402,7 +408,9 @@ acceptance(
"topic picker modal is open"
);
- const topicRadioBtn = queryAll('input[name="choose_topic_id"]:first');
+ const topicRadioBtn = queryAll(
+ 'input[name="choose_topic_id"]:nth-of-type(1)'
+ )[0];
assert.ok(exists(topicRadioBtn), "Topic options are prefilled");
await click(topicRadioBtn);
diff --git a/app/assets/javascripts/discourse/tests/acceptance/raw-plugin-outlet-test.js b/app/assets/javascripts/discourse/tests/acceptance/raw-plugin-outlet-test.js
index 13a2f52e1e..24d8055fb7 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/raw-plugin-outlet-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/raw-plugin-outlet-test.js
@@ -3,7 +3,7 @@ import {
addRawTemplate,
removeRawTemplate,
} from "discourse-common/lib/raw-templates";
-import compile from "handlebars-compiler";
+import { compile } from "handlebars";
import { test } from "qunit";
import { visit } from "@ember/test-helpers";
diff --git a/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js b/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js
index de8eaab54d..e4cfa4745f 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js
@@ -160,7 +160,9 @@ acceptance("Search - Full Page", function (needs) {
'"autocomplete" popup has an entry for "admin"'
);
- await click(".search-advanced-options .autocomplete ul li a:first");
+ await click(
+ ".search-advanced-options .autocomplete ul li a:nth-of-type(1)"
+ );
assert.ok(
exists('.search-advanced-options span:contains("admin")'),
diff --git a/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js b/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js
index 62df079754..9b6136663a 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js
@@ -154,10 +154,6 @@ acceptance("Signing In", function () {
"the username validation is bad"
);
await click(".modal-footer .btn-primary");
- assert.ok(
- exists("#new-account-username:focus"),
- "username field is focused"
- );
await fillIn("#new-account-username", "goodtuna");
assert.ok(
diff --git a/app/assets/javascripts/discourse/tests/acceptance/tag-groups-test.js b/app/assets/javascripts/discourse/tests/acceptance/tag-groups-test.js
index c3ab9d85d8..6d8de06da1 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/tag-groups-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/tag-groups-test.js
@@ -46,7 +46,7 @@ acceptance("Tag Groups", function (needs) {
await click(".tag-group-content .btn.btn-default");
- await click(".tag-chooser .choice:first");
+ await click(".tag-chooser .choice:nth-of-type(1)");
assert.ok(!queryAll(".tag-group-content .btn.btn-danger")[0].disabled);
});
diff --git a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js
index a3d339afec..31b22fc653 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js
@@ -102,7 +102,7 @@ acceptance("Tags listed by group", function (needs) {
"shown in given order and with tags that are not in a group"
);
assert.deepEqual(
- $(".tag-list:first .discourse-tag")
+ $(".tag-list:nth-of-type(1) .discourse-tag")
.toArray()
.map((i) => {
return $(i).text();
@@ -111,7 +111,7 @@ acceptance("Tags listed by group", function (needs) {
"shows the tags in default sort (by count)"
);
assert.deepEqual(
- $(".tag-list:first .discourse-tag")
+ $(".tag-list:nth-of-type(1) .discourse-tag")
.toArray()
.map((i) => {
return $(i).attr("href");
@@ -302,7 +302,7 @@ acceptance("Tag info", function (needs) {
"delete UI is visible"
);
- await click(".unlink-synonym:first");
+ await click(".unlink-synonym:nth-of-type(1)");
assert.ok(
queryAll(".tag-info .synonyms-list .tag-box").length === 1,
"removed a synonym"
diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-discovery-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-discovery-test.js
index 627da72912..f71e25020d 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/topic-discovery-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/topic-discovery-test.js
@@ -22,7 +22,7 @@ acceptance("Topic Discovery", function (needs) {
assert.ok(exists(".topic-list .topic-list-item"), "has topics");
assert.equal(
- queryAll("a[data-user-card=eviltrout]:first img.avatar").attr("title"),
+ queryAll("a[data-user-card=eviltrout] img.avatar").attr("title"),
"Evil Trout - Most Posts",
"it shows user's full name in avatar title"
);
diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-test.js
index 351d39e573..845d58e45d 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/topic-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/topic-test.js
@@ -1,17 +1,22 @@
import {
acceptance,
- exists,
queryAll,
visible,
} from "discourse/tests/helpers/qunit-helpers";
-import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers";
+import {
+ click,
+ fillIn,
+ settled,
+ triggerKeyEvent,
+ visit,
+} from "@ember/test-helpers";
import I18n from "I18n";
import selectKit from "discourse/tests/helpers/select-kit-helper";
import { test } from "qunit";
import { IMAGE_VERSION as v } from "pretty-text/emoji/version";
import { withPluginApi } from "discourse/lib/plugin-api";
-function selectText(selector) {
+async function selectText(selector) {
const range = document.createRange();
const node = document.querySelector(selector);
range.selectNodeContents(node);
@@ -19,6 +24,7 @@ function selectText(selector) {
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
+ await settled();
}
acceptance("Topic", function (needs) {
@@ -302,7 +308,7 @@ acceptance("Topic featured links", function (needs) {
await click(".toggle-admin-menu");
await click(".topic-admin-pin .btn");
- await click(".btn-primary:last");
+ await click(".make-banner");
await click(".toggle-admin-menu");
await click(".topic-admin-visible .btn");
@@ -362,7 +368,7 @@ acceptance("Topic featured links", function (needs) {
test("Quoting a quote keeps the original poster name", async function (assert) {
await visit("/t/internationalization-localization/280");
- selectText("#post_5 blockquote");
+ await selectText("#post_5 blockquote");
await click(".quote-button .insert-quote");
assert.ok(
@@ -374,7 +380,7 @@ acceptance("Topic featured links", function (needs) {
test("Quoting a quote of a different topic keeps the original topic title", async function (assert) {
await visit("/t/internationalization-localization/280");
- selectText("#post_9 blockquote");
+ await selectText("#post_9 blockquote");
await click(".quote-button .insert-quote");
assert.ok(
@@ -388,7 +394,7 @@ acceptance("Topic featured links", function (needs) {
test("Quoting a quote with the Reply button keeps the original poster name", async function (assert) {
await visit("/t/internationalization-localization/280");
- selectText("#post_5 blockquote");
+ await selectText("#post_5 blockquote");
await click(".reply");
assert.ok(
@@ -400,7 +406,7 @@ acceptance("Topic featured links", function (needs) {
test("Quoting a quote with replyAsNewTopic keeps the original poster name", async function (assert) {
await visit("/t/internationalization-localization/280");
- selectText("#post_5 blockquote");
+ await selectText("#post_5 blockquote");
await triggerKeyEvent(document, "keypress", "j".charCodeAt(0));
await triggerKeyEvent(document, "keypress", "t".charCodeAt(0));
@@ -413,7 +419,7 @@ acceptance("Topic featured links", function (needs) {
test("Quoting by selecting text can mark the quote as full", async function (assert) {
await visit("/t/internationalization-localization/280");
- selectText("#post_5 .cooked");
+ await selectText("#post_5 .cooked");
await click(".quote-button .insert-quote");
assert.ok(
diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js
index 3dff64d71d..2e9281f368 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/user-card-test.js
@@ -17,7 +17,7 @@ acceptance("User Card - Show Local Time", function (needs) {
User.current().changeTimezone("Australia/Brisbane");
await visit("/t/internationalization-localization/280");
- await click('a[data-user-card="charlie"]:first');
+ await click('a[data-user-card="charlie"]');
assert.not(
exists(".user-card .local-time"),
diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-test.js
index 2c747253a6..ae7603bf4d 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/user-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/user-test.js
@@ -15,7 +15,13 @@ acceptance("User Routes", function (needs) {
);
});
test("Invalid usernames", async function (assert) {
- await visit("/u/eviltrout%2F..%2F..%2F/summary");
+ try {
+ await visit("/u/eviltrout%2F..%2F..%2F/summary");
+ } catch (e) {
+ if (e.message !== "TransitionAborted") {
+ throw e;
+ }
+ }
assert.equal(currentRouteName(), "exception-unknown");
});
diff --git a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js
index b75d641d33..80997d6f07 100644
--- a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js
+++ b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js
@@ -144,6 +144,15 @@ export function applyDefaultHandlers(pretender) {
return response({ email: "eviltrout@example.com" });
});
+ pretender.get("/u/is_local_username", () =>
+ response({
+ valid: [],
+ valid_groups: [],
+ mentionable_groups: [],
+ cannot_see: [],
+ })
+ );
+
pretender.get("/u/eviltrout.json", () => {
const json = fixturesByUrl["/u/eviltrout.json"];
json.user.can_edit = loggedIn();
diff --git a/app/assets/javascripts/discourse/tests/unit/controllers/bookmark-test.js b/app/assets/javascripts/discourse/tests/unit/controllers/bookmark-test.js
index ca8a6b1856..e67342fbb4 100644
--- a/app/assets/javascripts/discourse/tests/unit/controllers/bookmark-test.js
+++ b/app/assets/javascripts/discourse/tests/unit/controllers/bookmark-test.js
@@ -6,6 +6,7 @@ import {
import KeyboardShortcutInitializer from "discourse/initializers/keyboard-shortcuts";
import { REMINDER_TYPES } from "discourse/lib/bookmark";
import User from "discourse/models/user";
+import { getApplication } from "@ember/test-helpers";
import sinon from "sinon";
import { test } from "qunit";
@@ -18,7 +19,7 @@ function mockMomentTz(dateString) {
discourseModule("Unit | Controller | bookmark", function (hooks) {
hooks.beforeEach(function () {
logIn();
- KeyboardShortcutInitializer.initialize(this.container);
+ KeyboardShortcutInitializer.initialize(getApplication());
BookmarkController = this.owner.lookup("controller:bookmark");
BookmarkController.setProperties({
diff --git a/app/assets/javascripts/discourse/tests/unit/localization-test.js b/app/assets/javascripts/discourse/tests/unit/localization-test.js
index 4939746aed..f2bf2f3c0f 100644
--- a/app/assets/javascripts/discourse/tests/unit/localization-test.js
+++ b/app/assets/javascripts/discourse/tests/unit/localization-test.js
@@ -1,6 +1,7 @@
import { module, test } from "qunit";
import I18n from "I18n";
import LocalizationInitializer from "discourse/initializers/localization";
+import { getApplication } from "@ember/test-helpers";
module("initializer:localization", {
_locale: I18n.locale,
@@ -42,7 +43,7 @@ test("translation overrides", function (assert) {
"js.composer.reply": "WAT",
"js.topic.reply.help": "foobar",
};
- LocalizationInitializer.initialize(this.registry);
+ LocalizationInitializer.initialize(getApplication());
assert.equal(
I18n.t("composer.reply"),
@@ -61,7 +62,7 @@ test("skip translation override if parent node is not an object", function (asse
"js.composer.reply": "WAT",
"js.composer.reply.help": "foobar",
};
- LocalizationInitializer.initialize(this.registry);
+ LocalizationInitializer.initialize(getApplication());
assert.equal(I18n.t("composer.reply.help"), "[fr.composer.reply.help]");
});
diff --git a/app/assets/javascripts/handlebars-shim.js b/app/assets/javascripts/handlebars-shim.js
index fb191149a1..d73b33ce45 100644
--- a/app/assets/javascripts/handlebars-shim.js
+++ b/app/assets/javascripts/handlebars-shim.js
@@ -5,6 +5,10 @@ if (typeof define !== "undefined") {
if (typeof Handlebars !== "undefined") {
// eslint-disable-next-line
__exports__.default = Handlebars;
+ __exports__.compile = function () {
+ // eslint-disable-next-line
+ return Handlebars.compile(...arguments);
+ };
}
});
From 5573d9eab9daac6abf40ca1d8e4c00b91b88cfb2 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 21 Jan 2021 22:37:11 +0100
Subject: [PATCH 03/85] Build(deps-dev): Bump test-prof from 0.12.2 to 1.0.0
(#11795)
Bumps [test-prof](https://github.com/test-prof/test-prof) from 0.12.2 to 1.0.0.
- [Release notes](https://github.com/test-prof/test-prof/releases)
- [Changelog](https://github.com/test-prof/test-prof/blob/master/CHANGELOG.md)
- [Commits](https://github.com/test-prof/test-prof/commits/v1.0.0)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Gemfile.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 3b16e87e10..ac588ccd5c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -416,7 +416,7 @@ GEM
sprockets (>= 3.0.0)
sshkey (2.0.0)
stackprof (0.2.16)
- test-prof (0.12.2)
+ test-prof (1.0.0)
thor (1.1.0)
thread_safe (0.3.6)
tilt (2.0.10)
From ecd51197aa1534ed9e4217249d9bdd1d79b9bd25 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 21 Jan 2021 22:38:03 +0100
Subject: [PATCH 04/85] Build(deps): Bump css_parser from 1.7.1 to 1.8.0
(#11797)
Bumps [css_parser](https://github.com/premailer/css_parser) from 1.7.1 to 1.8.0.
- [Release notes](https://github.com/premailer/css_parser/releases)
- [Changelog](https://github.com/premailer/css_parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/premailer/css_parser/compare/v1.7.1...v1.8.0)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Gemfile.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index ac588ccd5c..b7c6bcccef 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -93,7 +93,7 @@ GEM
crack (0.4.5)
rexml
crass (1.0.6)
- css_parser (1.7.1)
+ css_parser (1.8.0)
addressable
debug_inspector (1.0.0)
diff-lcs (1.4.4)
From a8c5ef7dff5cb548574d59be10e01aaf56e6d848 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 21 Jan 2021 22:38:35 +0100
Subject: [PATCH 05/85] Build(deps-dev): Bump bullet from 6.1.2 to 6.1.3
(#11798)
Bumps [bullet](https://github.com/flyerhzm/bullet) from 6.1.2 to 6.1.3.
- [Release notes](https://github.com/flyerhzm/bullet/releases)
- [Changelog](https://github.com/flyerhzm/bullet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/flyerhzm/bullet/compare/6.1.2...6.1.3)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Gemfile.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index b7c6bcccef..e671a04f95 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -75,7 +75,7 @@ GEM
bootsnap (1.5.1)
msgpack (~> 1.0)
builder (3.2.4)
- bullet (6.1.2)
+ bullet (6.1.3)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
byebug (11.1.3)
From 5cbb522c415c646890365983cb19e59812e53d02 Mon Sep 17 00:00:00 2001
From: Krzysztof Kotlarek
Date: Fri, 22 Jan 2021 08:43:14 +1100
Subject: [PATCH 06/85] FIX: broken URL when username contains subfolder.
(#11786)
The bug was mentioned on [meta](https://meta.discourse.org/t/two-bugs-with-usernames-starting-with-subfolder-name/169505)
When discourse is installed on `/subfolder` and username is containing subfolder name like for example `subfolderadmin` - user URLs were incorrect.
Instead of having `/subfolder/u/subfolderadmin/summary/` we were leading to `/subfolder/uadmin/summary`.
The reason for that was incorrect check in `getUrl` helper:
```javascript
const found = url.indexOf(baseUri);
if (found >= 0 && found < 3) {
return url;
}
return baseUri + url;
```
baseUri is `/subfolder`, url is `/u/subfolderadmin` and indexOf returned position which in the end returned incorrect URL.
I think that we should check if the URL starts with baseUri and not if contains baseUri.
---
.../javascripts/discourse-common/addon/lib/get-url.js | 4 ++--
.../javascripts/discourse/tests/unit/lib/get-url-test.js | 6 ++++++
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/app/assets/javascripts/discourse-common/addon/lib/get-url.js b/app/assets/javascripts/discourse-common/addon/lib/get-url.js
index 1981a38bfb..29a9e56ce1 100644
--- a/app/assets/javascripts/discourse-common/addon/lib/get-url.js
+++ b/app/assets/javascripts/discourse-common/addon/lib/get-url.js
@@ -15,9 +15,9 @@ export default function getURL(url) {
return url;
}
- const found = url.indexOf(baseUri);
+ const found = url.startsWith(baseUri);
- if (found >= 0 && found < 3) {
+ if (found) {
return url;
}
if (url[0] !== "/") {
diff --git a/app/assets/javascripts/discourse/tests/unit/lib/get-url-test.js b/app/assets/javascripts/discourse/tests/unit/lib/get-url-test.js
index 82d86b838a..626de26679 100644
--- a/app/assets/javascripts/discourse/tests/unit/lib/get-url-test.js
+++ b/app/assets/javascripts/discourse/tests/unit/lib/get-url-test.js
@@ -60,6 +60,12 @@ module("Unit | Utility | get-url", function () {
"relative url has subfolder"
);
+ assert.equal(
+ getURL("/u/forumadmin"),
+ "/forum/u/forumadmin",
+ "relative url has subfolder even if username contains subfolder"
+ );
+
assert.equal(
getURL(""),
"/forum",
From 872f3e6934e6dbfc300fd2194bd1436ee8502c36 Mon Sep 17 00:00:00 2001
From: Vinoth Kannan
Date: Fri, 22 Jan 2021 03:29:34 +0530
Subject: [PATCH 07/85] UX: warn about messages to be orphaned while deleting a
group. (#11727)
Currently, after destroying a group its messages are inaccessible to everyone. Only admins can access using direct URLs.
---
.../javascripts/discourse/app/controllers/group.js | 13 +++++++++++--
.../discourse/tests/acceptance/group-test.js | 12 ++++++++++++
.../discourse/tests/fixtures/group-fixtures.js | 4 +++-
app/models/group.rb | 5 +++++
app/serializers/group_show_serializer.rb | 3 ++-
config/locales/client.en.yml | 3 +++
spec/serializers/group_show_serializer_spec.rb | 1 +
7 files changed, 37 insertions(+), 4 deletions(-)
diff --git a/app/assets/javascripts/discourse/app/controllers/group.js b/app/assets/javascripts/discourse/app/controllers/group.js
index 635a1bca1e..3d10fc9f02 100644
--- a/app/assets/javascripts/discourse/app/controllers/group.js
+++ b/app/assets/javascripts/discourse/app/controllers/group.js
@@ -142,13 +142,22 @@ export default Controller.extend({
destroyGroup() {
this.set("destroying", true);
+ const model = this.model;
+ let message = I18n.t("admin.groups.delete_confirm");
+
+ if (model.has_messages && model.message_count > 0) {
+ message = I18n.t("admin.groups.delete_with_messages_confirm", {
+ count: model.message_count,
+ });
+ }
+
bootbox.confirm(
- I18n.t("admin.groups.delete_confirm"),
+ message,
I18n.t("no_value"),
I18n.t("yes_value"),
(confirmed) => {
if (confirmed) {
- this.model
+ model
.destroy()
.then(() => this.transitionToRoute("groups.index"))
.catch((error) => {
diff --git a/app/assets/javascripts/discourse/tests/acceptance/group-test.js b/app/assets/javascripts/discourse/tests/acceptance/group-test.js
index 6fba32ebd5..5164cd920d 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/group-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/group-test.js
@@ -261,6 +261,18 @@ acceptance("Group - Authenticated", function (needs) {
"Awesome Team",
"it should display the group name"
);
+
+ await click(".group-details-button button.btn-danger");
+
+ assert.equal(
+ queryAll(".bootbox .modal-body").html(),
+ I18n.t("admin.groups.delete_with_messages_confirm", {
+ count: 2,
+ }),
+ "it should warn about orphan messages"
+ );
+
+ await click(".modal-footer .btn-default");
});
test("Moderator Viewing Group", async function (assert) {
diff --git a/app/assets/javascripts/discourse/tests/fixtures/group-fixtures.js b/app/assets/javascripts/discourse/tests/fixtures/group-fixtures.js
index f1a7867be6..615fe75086 100644
--- a/app/assets/javascripts/discourse/tests/fixtures/group-fixtures.js
+++ b/app/assets/javascripts/discourse/tests/fixtures/group-fixtures.js
@@ -46,7 +46,9 @@ export default {
is_group_owner: true,
mentionable: true,
messageable: true,
- can_see_members: true
+ can_see_members: true,
+ has_messages: true,
+ message_count: 2
},
extras: {
visible_group_names: ["discourse"]
diff --git a/app/models/group.rb b/app/models/group.rb
index 4c601847ee..696703c187 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -838,6 +838,11 @@ class Group < ActiveRecord::Base
)
end
+ def message_count
+ return 0 unless self.has_messages
+ TopicAllowedGroup.where(group_id: self.id).joins(:topic).count
+ end
+
protected
def name_format_validator
diff --git a/app/serializers/group_show_serializer.rb b/app/serializers/group_show_serializer.rb
index 53a6f0d93d..584e20b1f0 100644
--- a/app/serializers/group_show_serializer.rb
+++ b/app/serializers/group_show_serializer.rb
@@ -25,7 +25,8 @@ class GroupShowSerializer < BasicGroupSerializer
:email_password,
:imap_last_error,
:imap_old_emails,
- :imap_new_emails
+ :imap_new_emails,
+ :message_count
def self.admin_or_owner_attributes(*attrs)
attributes(*attrs)
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index fefb293a43..f69a316fc8 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -3734,6 +3734,9 @@ en:
group_members: "Group members"
delete: "Delete"
delete_confirm: "Delete this group?"
+ delete_with_messages_confirm:
+ one: "Deleting this group will cause %{count} message to be orphaned, group members will no longer have access to it. Are you sure?"
+ other: "Deleting this group will cause %{count} messages to be orphaned, group members will no longer have access to them. Are you sure?"
delete_failed: "Unable to delete group. If this is an automatic group, it cannot be destroyed."
delete_owner_confirm: "Remove owner privilege for '%{username}'?"
add: "Add"
diff --git a/spec/serializers/group_show_serializer_spec.rb b/spec/serializers/group_show_serializer_spec.rb
index a6acacdfdb..00308ebb4c 100644
--- a/spec/serializers/group_show_serializer_spec.rb
+++ b/spec/serializers/group_show_serializer_spec.rb
@@ -79,6 +79,7 @@ describe GroupShowSerializer do
it 'are visible' do
expect(subject.as_json[:email_username]).to eq('foo@bar.com')
expect(subject.as_json[:email_password]).to eq('pa$$w0rd')
+ expect(subject.as_json[:message_count]).to eq(0)
end
end
end
From 74341169332a546765d5afa2db9df9fa4fb52763 Mon Sep 17 00:00:00 2001
From: Blake Erickson
Date: Thu, 21 Jan 2021 16:28:08 -0700
Subject: [PATCH 08/85] DEV: Add schema checking to api doc testing (#11721)
* DEV: Add schema checking to api doc testing
This commit improves upon rswag which lacks schema checking. rswag
really only checks that the https status matches, but this change adds
in the json-schema_builder gem which also has schema validation.
Now we can define schemas for each of our requests/responses in the
`spec/requests/api/schemas` directory which will make our documentation
specs a lot cleaner.
If we update a serializer by either adding or removing an attribute the
tests will now fail (this is a good thing!). Also if you change the type
of an attribute say from an array to a string the tests will now fail.
This will help significantly with keeping the docs in sync with actual
code changes! Now if you change how an endpoint will respond you will
have to update the docs too in order for the tests to pass. :D
This PR is inspired by:
https://www.tealhq.com/post/how-teal-keeps-their-api-tests-and-documentation-in-sync
* Swap out json schema validator gem
Swapped out the outdated json-schema_builder gem with the json_schemer
gem.
* Add validation fields to schema
In order to have "strict" validation we need to add
`additionalProperties: false` to the schema, and we need to specify
which attributes are required.
Updated the debugging test output to print out the error details if
there are any.
---
Gemfile | 1 +
Gemfile.lock | 10 +++
.../requests/api/schemas/tag_group_schemas.rb | 77 +++++++++++++++++++
spec/requests/api/shared/shared_examples.rb | 41 ++++++++++
spec/requests/api/tags_spec.rb | 46 +++--------
spec/swagger_helper.rb | 7 ++
6 files changed, 147 insertions(+), 35 deletions(-)
create mode 100644 spec/requests/api/schemas/tag_group_schemas.rb
create mode 100644 spec/requests/api/shared/shared_examples.rb
diff --git a/Gemfile b/Gemfile
index 3ea59bc92e..80bf241db0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -167,6 +167,7 @@ group :test, :development do
gem 'parallel_tests'
gem 'rswag-specs'
+ gem 'json_schemer'
end
group :development do
diff --git a/Gemfile.lock b/Gemfile.lock
index e671a04f95..56f0b0c4e6 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -114,6 +114,8 @@ GEM
in_threads (~> 1.3)
progress (~> 3.0, >= 3.0.1)
docile (1.3.5)
+ ecma-re-validator (0.3.0)
+ regexp_parser (~> 2.0)
email_reply_trimmer (0.1.13)
ember-data-source (3.0.2)
ember-source (>= 2, < 3.0)
@@ -141,6 +143,7 @@ GEM
globalid (0.4.2)
activesupport (>= 4.2.0)
guess_html_encoding (0.0.11)
+ hana (1.3.7)
hashdiff (1.0.1)
hashie (4.1.0)
highline (2.0.3)
@@ -159,6 +162,11 @@ GEM
json (2.5.1)
json-schema (2.8.1)
addressable (>= 2.4)
+ json_schemer (0.2.17)
+ ecma-re-validator (~> 0.3)
+ hana (~> 1.3)
+ regexp_parser (~> 2.0)
+ uri_template (~> 0.7)
jwt (2.2.2)
kgio (2.11.3)
libv8 (8.4.255.0)
@@ -432,6 +440,7 @@ GEM
kgio (~> 2.6)
raindrops (~> 0.7)
uniform_notifier (1.13.2)
+ uri_template (0.7.0)
webmock (3.11.1)
addressable (>= 2.3.6)
crack (>= 0.3.2)
@@ -494,6 +503,7 @@ DEPENDENCIES
htmlentities
http_accept_language
json
+ json_schemer
listen
lograge
logstash-event
diff --git a/spec/requests/api/schemas/tag_group_schemas.rb b/spec/requests/api/schemas/tag_group_schemas.rb
new file mode 100644
index 0000000000..86fd718de6
--- /dev/null
+++ b/spec/requests/api/schemas/tag_group_schemas.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module SpecSchemas
+
+ class TagGroupCreateRequest
+ def schemer
+ schema = {
+ 'type' => 'object',
+ 'additionalProperties' => false,
+ 'properties' => {
+ 'name' => {
+ 'type' => 'string',
+ }
+ },
+ 'required' => ['name']
+ }
+ end
+ end
+
+ class TagGroupResponse
+ def schemer
+ schema = {
+ 'type' => 'object',
+ 'additionalProperties' => false,
+ 'properties' => {
+ 'tag_group' => {
+ 'type' => 'object',
+ 'properties' => {
+ 'id' => {
+ 'type' => 'integer',
+ },
+ 'name' => {
+ 'type' => 'string',
+ },
+ 'tag_names' => {
+ 'type' => 'array',
+ 'items' => {
+ 'type' => 'string'
+ }
+ },
+ 'parent_tag_name' => {
+ 'type' => 'array',
+ 'items' => {
+ 'type' => 'string'
+ }
+ },
+ 'one_per_topic' => {
+ 'type' => 'boolean',
+ },
+ 'permissions' => {
+ 'type' => 'object',
+ 'properties' => {
+ 'everyone' => {
+ 'type' => 'integer',
+ 'example' => 1
+ }
+ }
+ }
+ },
+ 'required' => [
+ 'id',
+ 'name',
+ 'tag_names',
+ 'parent_tag_name',
+ 'one_per_topic',
+ 'permissions'
+ ]
+ }
+ },
+ 'required' => [
+ 'tag_group'
+ ]
+ }
+ end
+ end
+
+end
diff --git a/spec/requests/api/shared/shared_examples.rb b/spec/requests/api/shared/shared_examples.rb
new file mode 100644
index 0000000000..a4361e2758
--- /dev/null
+++ b/spec/requests/api/shared/shared_examples.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples "a JSON endpoint" do |expected_response_status|
+ before do |example|
+ submit_request(example.metadata)
+ end
+
+ describe "response status" do
+ it "returns expected response status" do
+ expect(response.status).to eq(expected_response_status)
+ end
+ end
+
+ describe "request body" do
+ it "matches the documented request schema" do |example|
+ schemer = JSONSchemer.schema(expected_request_schema.schemer)
+ valid = schemer.valid?(params)
+ unless valid # for debugging
+ puts params
+ puts schemer.validate(params).to_a[0]["details"]
+ end
+ expect(valid).to eq(true)
+ end
+ end
+
+ describe "response body" do
+ let(:json_response) { JSON.parse(response.body) }
+
+ it "matches the documented response schema" do |example|
+ schemer = JSONSchemer.schema(
+ expected_response_schema.schemer,
+ )
+ valid = schemer.valid?(json_response)
+ unless valid # for debugging
+ puts json_response
+ puts schemer.validate(json_response).to_a[0]["details"]
+ end
+ expect(valid).to eq(true)
+ end
+ end
+end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index b5c53cf4fb..e599af201a 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -60,46 +60,22 @@ describe 'tags' do
post 'Creates a tag group' do
tags 'Tags'
consumes 'application/json'
- parameter name: :post_body, in: :body, schema: {
- type: :object,
- properties: {
- name: { type: :string }
- },
- required: [ 'name' ]
- }
+ expected_request_schema = SpecSchemas::TagGroupCreateRequest.new
+
+ parameter name: :params, in: :body, schema: expected_request_schema.schemer
produces 'application/json'
response '200', 'tag group created' do
- schema type: :object, properties: {
- tag_group: {
- type: :object,
- properties: {
- id: { type: :integer },
- name: { type: :string },
- tag_names: {
- type: :array,
- items: {
- },
- },
- parent_tag_name: {
- type: :array,
- items: {
- },
- },
- one_per_topic: { type: :boolean },
- permissions: {
- type: :object,
- properties: {
- everyone: { type: :integer },
- }
- },
- }
- },
- }
+ expected_response_schema = SpecSchemas::TagGroupResponse.new
- let(:post_body) { { name: 'todo' } }
+ let(:params) { { 'name' => 'todo' } }
- run_test!
+ schema(expected_response_schema.schemer)
+
+ it_behaves_like "a JSON endpoint", 200 do
+ let(:expected_response_schema) { expected_response_schema }
+ let(:expected_request_schema) { expected_request_schema }
+ end
end
end
diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb
index 12d9f54718..d40966a961 100644
--- a/spec/swagger_helper.rb
+++ b/spec/swagger_helper.rb
@@ -1,6 +1,13 @@
# frozen_string_literal: true
require 'rails_helper'
+require 'json_schemer'
+
+# Require schema files
+Dir["./spec/requests/api/schemas/*.rb"].each { |file| require file }
+
+# Require shared spec examples
+Dir["./spec/requests/api/shared/*.rb"].each { |file| require file }
RSpec.configure do |config|
# Specify a root folder where Swagger JSON files are generated
From c889b676f80565b4b65dfe45d53e93cb6ff9d885 Mon Sep 17 00:00:00 2001
From: Blake Erickson
Date: Thu, 21 Jan 2021 18:23:23 -0700
Subject: [PATCH 09/85] DEV: Updates to api docs schema validation (#11801)
- Read in schemas from actual json files instead of a ruby hash. This is
helpful because we will be automatically generating .json schema files
from json responses and don't want to manually write ruby hash schema
files.
- Create a helper method for rspec schema validation tests to dry up code
---
.../json/tag_group_create_request.json | 12 +++
.../json/tag_group_create_response.json | 51 ++++++++++++
spec/requests/api/schemas/schema_loader.rb | 18 +++++
.../requests/api/schemas/tag_group_schemas.rb | 77 -------------------
spec/requests/api/shared/shared_examples.rb | 28 +++----
spec/requests/api/tags_spec.rb | 8 +-
6 files changed, 99 insertions(+), 95 deletions(-)
create mode 100644 spec/requests/api/schemas/json/tag_group_create_request.json
create mode 100644 spec/requests/api/schemas/json/tag_group_create_response.json
create mode 100644 spec/requests/api/schemas/schema_loader.rb
delete mode 100644 spec/requests/api/schemas/tag_group_schemas.rb
diff --git a/spec/requests/api/schemas/json/tag_group_create_request.json b/spec/requests/api/schemas/json/tag_group_create_request.json
new file mode 100644
index 0000000000..2e364e13d9
--- /dev/null
+++ b/spec/requests/api/schemas/json/tag_group_create_request.json
@@ -0,0 +1,12 @@
+{
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ]
+}
diff --git a/spec/requests/api/schemas/json/tag_group_create_response.json b/spec/requests/api/schemas/json/tag_group_create_response.json
new file mode 100644
index 0000000000..7d04b1663f
--- /dev/null
+++ b/spec/requests/api/schemas/json/tag_group_create_response.json
@@ -0,0 +1,51 @@
+{
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "tag_group": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "tag_names": {
+ "type": "array",
+ "items": [
+
+ ]
+ },
+ "parent_tag_name": {
+ "type": "array",
+ "items": [
+
+ ]
+ },
+ "one_per_topic": {
+ "type": "boolean"
+ },
+ "permissions": {
+ "type": "object",
+ "properties": {
+ "everyone": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "tag_names",
+ "parent_tag_name",
+ "one_per_topic",
+ "permissions"
+ ]
+ }
+ },
+ "required": [
+ "tag_group"
+ ]
+}
diff --git a/spec/requests/api/schemas/schema_loader.rb b/spec/requests/api/schemas/schema_loader.rb
new file mode 100644
index 0000000000..872e47d74a
--- /dev/null
+++ b/spec/requests/api/schemas/schema_loader.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'json'
+
+module SpecSchemas
+
+ class SpecLoader
+
+ def initialize(filename)
+ @filename = filename
+ end
+
+ def load
+ JSON.parse(File.read(File.join(__dir__, "json", "#{@filename}.json")))
+ end
+ end
+
+end
diff --git a/spec/requests/api/schemas/tag_group_schemas.rb b/spec/requests/api/schemas/tag_group_schemas.rb
deleted file mode 100644
index 86fd718de6..0000000000
--- a/spec/requests/api/schemas/tag_group_schemas.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-# frozen_string_literal: true
-
-module SpecSchemas
-
- class TagGroupCreateRequest
- def schemer
- schema = {
- 'type' => 'object',
- 'additionalProperties' => false,
- 'properties' => {
- 'name' => {
- 'type' => 'string',
- }
- },
- 'required' => ['name']
- }
- end
- end
-
- class TagGroupResponse
- def schemer
- schema = {
- 'type' => 'object',
- 'additionalProperties' => false,
- 'properties' => {
- 'tag_group' => {
- 'type' => 'object',
- 'properties' => {
- 'id' => {
- 'type' => 'integer',
- },
- 'name' => {
- 'type' => 'string',
- },
- 'tag_names' => {
- 'type' => 'array',
- 'items' => {
- 'type' => 'string'
- }
- },
- 'parent_tag_name' => {
- 'type' => 'array',
- 'items' => {
- 'type' => 'string'
- }
- },
- 'one_per_topic' => {
- 'type' => 'boolean',
- },
- 'permissions' => {
- 'type' => 'object',
- 'properties' => {
- 'everyone' => {
- 'type' => 'integer',
- 'example' => 1
- }
- }
- }
- },
- 'required' => [
- 'id',
- 'name',
- 'tag_names',
- 'parent_tag_name',
- 'one_per_topic',
- 'permissions'
- ]
- }
- },
- 'required' => [
- 'tag_group'
- ]
- }
- end
- end
-
-end
diff --git a/spec/requests/api/shared/shared_examples.rb b/spec/requests/api/shared/shared_examples.rb
index a4361e2758..f6e732dce5 100644
--- a/spec/requests/api/shared/shared_examples.rb
+++ b/spec/requests/api/shared/shared_examples.rb
@@ -5,6 +5,16 @@ RSpec.shared_examples "a JSON endpoint" do |expected_response_status|
submit_request(example.metadata)
end
+ def expect_schema_valid(schemer, params)
+ valid = schemer.valid?(params)
+ unless valid # for debugging
+ puts
+ puts "RESPONSE: #{params}"
+ puts "VALIDATION DETAILS: #{schemer.validate(params).to_a[0]["details"]}"
+ end
+ expect(valid).to eq(true)
+ end
+
describe "response status" do
it "returns expected response status" do
expect(response.status).to eq(expected_response_status)
@@ -13,13 +23,8 @@ RSpec.shared_examples "a JSON endpoint" do |expected_response_status|
describe "request body" do
it "matches the documented request schema" do |example|
- schemer = JSONSchemer.schema(expected_request_schema.schemer)
- valid = schemer.valid?(params)
- unless valid # for debugging
- puts params
- puts schemer.validate(params).to_a[0]["details"]
- end
- expect(valid).to eq(true)
+ schemer = JSONSchemer.schema(expected_request_schema)
+ expect_schema_valid(schemer, params)
end
end
@@ -28,14 +33,9 @@ RSpec.shared_examples "a JSON endpoint" do |expected_response_status|
it "matches the documented response schema" do |example|
schemer = JSONSchemer.schema(
- expected_response_schema.schemer,
+ expected_response_schema,
)
- valid = schemer.valid?(json_response)
- unless valid # for debugging
- puts json_response
- puts schemer.validate(json_response).to_a[0]["details"]
- end
- expect(valid).to eq(true)
+ expect_schema_valid(schemer, json_response)
end
end
end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index e599af201a..b868135b20 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -60,17 +60,17 @@ describe 'tags' do
post 'Creates a tag group' do
tags 'Tags'
consumes 'application/json'
- expected_request_schema = SpecSchemas::TagGroupCreateRequest.new
+ expected_request_schema = SpecSchemas::SpecLoader.new('tag_group_create_request').load
- parameter name: :params, in: :body, schema: expected_request_schema.schemer
+ parameter name: :params, in: :body, schema: expected_request_schema
produces 'application/json'
response '200', 'tag group created' do
- expected_response_schema = SpecSchemas::TagGroupResponse.new
+ expected_response_schema = SpecSchemas::SpecLoader.new('tag_group_create_response').load
let(:params) { { 'name' => 'todo' } }
- schema(expected_response_schema.schemer)
+ schema(expected_response_schema)
it_behaves_like "a JSON endpoint", 200 do
let(:expected_response_schema) { expected_response_schema }
From ff095e7249dfef3b06a9a6b959283546819d89a1 Mon Sep 17 00:00:00 2001
From: Kris
Date: Thu, 21 Jan 2021 21:58:06 -0500
Subject: [PATCH 10/85] A11Y: Update selected name role to button (#11804)
---
.../select-kit/addon/templates/components/selected-name.hbs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/assets/javascripts/select-kit/addon/templates/components/selected-name.hbs b/app/assets/javascripts/select-kit/addon/templates/components/selected-name.hbs
index da45408eac..058c151b5d 100644
--- a/app/assets/javascripts/select-kit/addon/templates/components/selected-name.hbs
+++ b/app/assets/javascripts/select-kit/addon/templates/components/selected-name.hbs
@@ -1,5 +1,5 @@
{{#if selectKit.options.showFullTitle}}
-
+
{{#if item.icon}}
{{d-icon item.icon}}
{{/if}}
From e031679f99b1db1db016279c723bc69abc370b7d Mon Sep 17 00:00:00 2001
From: Kris
Date: Thu, 21 Jan 2021 21:58:34 -0500
Subject: [PATCH 11/85] A11Y: Add aria-label to input clear button (#11803)
---
.../addon/templates/components/combo-box/combo-box-header.hbs | 1 +
.../select-kit/addon/templates/components/selected-name.hbs | 1 +
config/locales/client.en.yml | 1 +
3 files changed, 3 insertions(+)
diff --git a/app/assets/javascripts/select-kit/addon/templates/components/combo-box/combo-box-header.hbs b/app/assets/javascripts/select-kit/addon/templates/components/combo-box/combo-box-header.hbs
index 5213bceac9..59504133b9 100644
--- a/app/assets/javascripts/select-kit/addon/templates/components/combo-box/combo-box-header.hbs
+++ b/app/assets/javascripts/select-kit/addon/templates/components/combo-box/combo-box-header.hbs
@@ -11,6 +11,7 @@
class="btn-clear"
icon="times"
action=selectKit.onClearSelection
+ ariaLabel="clear_input"
}}
{{/if}}
diff --git a/app/assets/javascripts/select-kit/addon/templates/components/selected-name.hbs b/app/assets/javascripts/select-kit/addon/templates/components/selected-name.hbs
index 058c151b5d..c826cea9a7 100644
--- a/app/assets/javascripts/select-kit/addon/templates/components/selected-name.hbs
+++ b/app/assets/javascripts/select-kit/addon/templates/components/selected-name.hbs
@@ -17,6 +17,7 @@
icon="times"
action=selectKit.deselect
actionParam=item
+ ariaLabel="clear_input"
}}
{{/if}}
{{/if}}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index f69a316fc8..c5492d634c 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -210,6 +210,7 @@ en:
us_west_1: "US West (N. California)"
us_west_2: "US West (Oregon)"
+ clear_input: "Clear input"
edit: "edit the title and category of this topic"
expand: "Expand"
not_implemented: "That feature hasn't been implemented yet, sorry!"
From 9e6ff9cc67ca6f7a7fcbf7fd43d3f2b50f8ed899 Mon Sep 17 00:00:00 2001
From: Kris
Date: Thu, 21 Jan 2021 22:24:15 -0500
Subject: [PATCH 12/85] A11Y: associate search controls with their labels
(#11806)
---
.../app/templates/components/search-advanced-options.hbs | 7 +++++--
.../discourse/tests/acceptance/search-full-test.js | 4 ++--
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs b/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs
index c717b704d2..0920c12f77 100644
--- a/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs
@@ -7,6 +7,7 @@
{{user-chooser
+ id="search-posted-by"
value=searchedTerms.username
onChange=(action "onChangeSearchTermForUsername")
options=(hash
@@ -20,6 +21,7 @@
{{i18n "search.advanced.in_category.label"}}
{{search-advanced-category-chooser
+ id="search-in-category"
value=searchedTerms.category.id
onChange=(action "onChangeSearchTermForCategory")
}}
@@ -33,6 +35,7 @@
{{i18n "search.advanced.with_tags.label"}}
{{tag-chooser
+ id="search-with-tags"
tags=searchedTerms.tags
allowCreate=false
everyTag=true
@@ -116,7 +119,7 @@
{{i18n "search.advanced.statuses.label"}}
{{combo-box
- id="status"
+ id="search-status-options"
valueProperty="value"
content=statusOptions
value=searchedTerms.status
@@ -179,7 +182,7 @@
-
{{i18n "search.advanced.views.label"}}
+
{{i18n "search.advanced.views.label"}}
{{input
diff --git a/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js b/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js
index e4cfa4745f..48f0d1f9ed 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js
@@ -310,7 +310,7 @@ acceptance("Search - Full Page", function (needs) {
test("update status through advanced search ui", async function (assert) {
const statusSelector = selectKit(
- ".search-advanced-options .select-kit#status"
+ ".search-advanced-options .select-kit#search-status-options"
);
await visit("/search");
@@ -333,7 +333,7 @@ acceptance("Search - Full Page", function (needs) {
test("doesn't update status filter header if wrong value entered through searchbox", async function (assert) {
const statusSelector = selectKit(
- ".search-advanced-options .select-kit#status"
+ ".search-advanced-options .select-kit#search-status-options"
);
await visit("/search");
From d2cf43a7d54aef95f8777b22e92c42ae0fbe3f7f Mon Sep 17 00:00:00 2001
From: Bianca Nenciu
Date: Fri, 22 Jan 2021 10:21:09 +0200
Subject: [PATCH 13/85] FIX: Update categories without full page refresh
(#11793)
Creating or moving a category required a full page refresh until it
showed up correctly.
---
.../discourse/app/controllers/reorder-categories.js | 2 +-
app/models/category_list.rb | 2 +-
app/serializers/category_detailed_serializer.rb | 4 ----
3 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/app/assets/javascripts/discourse/app/controllers/reorder-categories.js b/app/assets/javascripts/discourse/app/controllers/reorder-categories.js
index 0875848ce3..c0b7778018 100644
--- a/app/assets/javascripts/discourse/app/controllers/reorder-categories.js
+++ b/app/assets/javascripts/discourse/app/controllers/reorder-categories.js
@@ -15,7 +15,7 @@ export default Controller.extend(ModalFunctionality, Evented, {
this.categoriesSorting = ["position"];
},
- @discourseComputed("site.categories")
+ @discourseComputed("site.categories.[]")
categoriesBuffered(categories) {
const bufProxy = EmberObjectProxy.extend(BufferedProxy);
return categories.map((c) => bufProxy.create({ content: c }));
diff --git a/app/models/category_list.rb b/app/models/category_list.rb
index 306dfa3d39..4f7d2c0610 100644
--- a/app/models/category_list.rb
+++ b/app/models/category_list.rb
@@ -109,7 +109,7 @@ class CategoryList < DraftableList
to_delete << c
end
end
- @categories.each { |c| c.subcategory_ids = subcategories[c.id] }
+ @categories.each { |c| c.subcategory_ids = subcategories[c.id] || [] }
@categories.delete_if { |c| to_delete.include?(c) }
end
diff --git a/app/serializers/category_detailed_serializer.rb b/app/serializers/category_detailed_serializer.rb
index 483f78f845..6b1e5083b2 100644
--- a/app/serializers/category_detailed_serializer.rb
+++ b/app/serializers/category_detailed_serializer.rb
@@ -26,10 +26,6 @@ class CategoryDetailedSerializer < BasicCategorySerializer
is_uncategorized
end
- def include_subcategory_ids?
- subcategory_ids.present?
- end
-
def topics_day
count_with_subcategories(:topics_day)
end
From e81d93cf2682c7bcca82ca4620804db7c37200be Mon Sep 17 00:00:00 2001
From: Arpit Jalan
Date: Fri, 22 Jan 2021 19:02:23 +0530
Subject: [PATCH 14/85] UX: specify width and height for onebox preview error
image (#11807)
---
app/assets/stylesheets/common/base/onebox.scss | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss
index 750d1627f2..aefd5bf5e8 100644
--- a/app/assets/stylesheets/common/base/onebox.scss
+++ b/app/assets/stylesheets/common/base/onebox.scss
@@ -322,7 +322,8 @@ aside.onebox {
}
}
-aside.onebox .onebox-body .onebox-avatar {
+aside.onebox .onebox-body .onebox-avatar,
+aside.onebox.preview-error .site-icon {
max-height: none;
max-width: none;
height: 60px;
From 7521cb51c459b8e57532f46f1dd0f1a55a8de5bb Mon Sep 17 00:00:00 2001
From: Joffrey JAFFEUX
Date: Fri, 22 Jan 2021 14:35:17 +0100
Subject: [PATCH 15/85] A11y: makes advanced search and html heading (#11808)
---
.../javascripts/discourse/app/templates/full-page-search.hbs | 4 ++--
app/assets/stylesheets/common/base/search.scss | 1 +
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/app/assets/javascripts/discourse/app/templates/full-page-search.hbs b/app/assets/javascripts/discourse/app/templates/full-page-search.hbs
index fcbd8c506d..c836ab9e16 100644
--- a/app/assets/javascripts/discourse/app/templates/full-page-search.hbs
+++ b/app/assets/javascripts/discourse/app/templates/full-page-search.hbs
@@ -203,9 +203,9 @@
{{i18n "search.advanced.title"}}
{{else}}
-
+
{{i18n "search.advanced.title"}}
-
+
{{/if}}
{{#if site.mobileView}}
diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss
index a6154bfc19..52dfdcd1b4 100644
--- a/app/assets/stylesheets/common/base/search.scss
+++ b/app/assets/stylesheets/common/base/search.scss
@@ -128,6 +128,7 @@
}
.search-advanced-title {
+ font-size: $font-up-1;
background: var(--primary-low);
padding: 0.358em 1em;
@include breakpoint(medium) {
From 314e7be2b1f2dfd6373e9efd4d65671e3062b9c7 Mon Sep 17 00:00:00 2001
From: Joffrey JAFFEUX
Date: Fri, 22 Jan 2021 14:39:16 +0100
Subject: [PATCH 16/85] A11Y: improves search-in-options filter accessibility
(#11809)
---
.../components/search-advanced-options.hbs | 66 +++++++++++--------
.../stylesheets/common/base/discourse.scss | 21 ++++++
2 files changed, 59 insertions(+), 28 deletions(-)
diff --git a/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs b/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs
index 0920c12f77..03b0a9a4d8 100644
--- a/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs
@@ -60,59 +60,69 @@
-
{{i18n "search.advanced.filters.label"}}
- {{#if currentUser}}
-
-
+
+ {{i18n "search.advanced.filters.label"}}
+
+ {{#if currentUser}}
+
{{input
+ id="matching-title-only"
type="checkbox"
class="in-title"
checked=searchedTerms.special.in.title
click=(action "onChangeSearchTermForSpecialInTitle" value="target.checked")
}}
- {{i18n "search.advanced.filters.title"}}
-
-
+
+ {{i18n "search.advanced.filters.title"}}
+
+
+
+
{{input
+ id="matching-liked"
type="checkbox"
class="in-likes"
checked=searchedTerms.special.in.likes
click=(action "onChangeSearchTermForSpecialInLikes" value="target.checked")
}}
- {{i18n "search.advanced.filters.likes"}}
-
-
+ {{i18n "search.advanced.filters.likes"}}
+
+
+
{{input
+ id="matching-in-messages"
type="checkbox"
class="in-private"
checked=searchedTerms.special.in.personal
click=(action "onChangeSearchTermForSpecialInPersonal" value="target.checked")
}}
- {{i18n "search.advanced.filters.private"}}
-
-
+ {{i18n "search.advanced.filters.private"}}
+
+
+
{{input
+ id="matching-seen"
type="checkbox"
class="in-seen"
checked=searchedTerms.special.in.seen
click=(action "onChangeSearchTermForSpecialInSeen" value="target.checked")
}}
- {{i18n "search.advanced.filters.seen"}}
-
-
- {{/if}}
- {{combo-box
- id="in"
- valueProperty="value"
- content=inOptions
- value=searchedTerms.in
- onChange=(action "onChangeSearchTermForIn")
- options=(hash
- none="user.locale.any"
- clearable=true
- )
- }}
+ {{i18n "search.advanced.filters.seen"}}
+
+ {{/if}}
+ {{combo-box
+ id="in"
+ valueProperty="value"
+ content=inOptions
+ value=searchedTerms.in
+ onChange=(action "onChangeSearchTermForIn")
+ options=(hash
+ none="user.locale.any"
+ clearable=true
+ )
+ }}
+
diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss
index 9bdd3be77a..ebaeaa087d 100644
--- a/app/assets/stylesheets/common/base/discourse.scss
+++ b/app/assets/stylesheets/common/base/discourse.scss
@@ -859,3 +859,24 @@ table {
z-index: z("max");
color: var(--secondary);
}
+
+.controls {
+ .grouped-control {
+ display: flex;
+ flex-direction: column;
+
+ .grouped-control-label {
+ padding: 0.25em 0;
+ }
+
+ .grouped-control-field {
+ flex: 1 0 auto;
+ display: flex;
+ padding-bottom: 0.25em;
+
+ label {
+ margin: 0;
+ }
+ }
+ }
+}
From 039b4111e36cde9a06e571289df89dd084cef6ab Mon Sep 17 00:00:00 2001
From: Vinoth Kannan
Date: Fri, 22 Jan 2021 19:48:01 +0530
Subject: [PATCH 17/85] FIX: print raw html of logo image to skip unwanted html
encoding (#11805)
Currently, the image logo is broken since the image tag is rendering incorrectly.
---
.../lib/discourse_narrative_bot/templates/advanced_user.svg.erb | 2 +-
.../lib/discourse_narrative_bot/templates/new_user.svg.erb | 2 +-
.../spec/requests/discobot_certificate_spec.rb | 1 +
3 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/advanced_user.svg.erb b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/advanced_user.svg.erb
index e5bccc0823..040bb7d9b0 100644
--- a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/advanced_user.svg.erb
+++ b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/advanced_user.svg.erb
@@ -46,7 +46,7 @@
<%= @name %>
- <%= @logo_group %>
+ <%== @logo_group %>
diff --git a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/new_user.svg.erb b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/new_user.svg.erb
index 298f6119c7..d89108ae8e 100644
--- a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/new_user.svg.erb
+++ b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/templates/new_user.svg.erb
@@ -457,7 +457,7 @@
<%= @name %>
- <%= @logo_group %>
+ <%== @logo_group %>
diff --git a/plugins/discourse-narrative-bot/spec/requests/discobot_certificate_spec.rb b/plugins/discourse-narrative-bot/spec/requests/discobot_certificate_spec.rb
index d30c76cf6a..54a7f5399c 100644
--- a/plugins/discourse-narrative-bot/spec/requests/discobot_certificate_spec.rb
+++ b/plugins/discourse-narrative-bot/spec/requests/discobot_certificate_spec.rb
@@ -36,6 +36,7 @@ describe "Discobot Certificate" do
get '/discobot/certificate.svg', params: params
expect(response.status).to eq(200)
+ expect(response.body).to include(' ')
end
describe 'when params are missing' do
From 15b5bd4e145dbb71102b476b7cb6732c4111a52b Mon Sep 17 00:00:00 2001
From: Arpit Jalan
Date: Fri, 22 Jan 2021 20:05:28 +0530
Subject: [PATCH 18/85] UX: show onebox error preview image as favicon (#11810)
---
app/assets/stylesheets/common/base/onebox.scss | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss
index aefd5bf5e8..b8c0a81bba 100644
--- a/app/assets/stylesheets/common/base/onebox.scss
+++ b/app/assets/stylesheets/common/base/onebox.scss
@@ -322,8 +322,7 @@ aside.onebox {
}
}
-aside.onebox .onebox-body .onebox-avatar,
-aside.onebox.preview-error .site-icon {
+aside.onebox .onebox-body .onebox-avatar {
max-height: none;
max-width: none;
height: 60px;
@@ -804,3 +803,9 @@ aside.onebox.stackexchange .onebox-body {
}
}
}
+
+aside.onebox.preview-error .site-icon {
+ width: 16px;
+ height: 16px;
+ margin-right: 0.5em;
+}
From 5a5f6905d7f13be7d1d7424a3af9e49662da62b8 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 22 Jan 2021 09:41:23 -0500
Subject: [PATCH 19/85] Build(deps): Bump logster from 2.9.4 to 2.9.5 (#11796)
Bumps [logster](https://github.com/discourse/logster) from 2.9.4 to 2.9.5.
- [Release notes](https://github.com/discourse/logster/releases)
- [Changelog](https://github.com/discourse/logster/blob/master/CHANGELOG.md)
- [Commits](https://github.com/discourse/logster/compare/v2.9.4...v2.9.5)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Gemfile.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 56f0b0c4e6..9c2418f5af 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -185,7 +185,7 @@ GEM
logstash-event (1.2.02)
logstash-logger (0.26.1)
logstash-event (~> 1.2)
- logster (2.9.4)
+ logster (2.9.5)
loofah (2.9.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
From f2fcc3d21a72adc42f7fb4eaa5d5f26c3da2c267 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 22 Jan 2021 09:42:34 -0500
Subject: [PATCH 20/85] Build(deps): Bump sidekiq from 6.1.2 to 6.1.3 (#11794)
Bumps [sidekiq](https://github.com/mperham/sidekiq) from 6.1.2 to 6.1.3.
- [Release notes](https://github.com/mperham/sidekiq/releases)
- [Changelog](https://github.com/mperham/sidekiq/blob/master/Changes.md)
- [Commits](https://github.com/mperham/sidekiq/compare/v6.1.2...v6.1.3)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Gemfile.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 9c2418f5af..9f569692c8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -405,7 +405,7 @@ GEM
activesupport (>= 3.1)
shoulda-matchers (4.5.0)
activesupport (>= 4.2.0)
- sidekiq (6.1.2)
+ sidekiq (6.1.3)
connection_pool (>= 2.2.2)
rack (~> 2.0)
redis (>= 4.2.0)
From 436c8e9bcd764af70778a36c0e40b608a1b29c7a Mon Sep 17 00:00:00 2001
From: Joffrey JAFFEUX
Date: Fri, 22 Jan 2021 15:48:23 +0100
Subject: [PATCH 21/85] DEV: eslint rules should be defined in
eslint-config-discourse (#11812)
---
.eslintrc | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/.eslintrc b/.eslintrc
index 320310db8d..e05e8de173 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,9 +1,7 @@
{
"extends": "eslint-config-discourse",
"rules": {
- "discourse-ember/global-ember": 2,
- "no-duplicate-imports": 2,
- "sort-imports": 2
+ "discourse-ember/global-ember": 2
},
"globals": {
"moduleFor": "off",
@@ -14,6 +12,6 @@
"currentURL": "off",
"invisible": "off",
"visible": "off",
- "count": "off",
+ "count": "off"
}
}
From 71656d2c37a3e89f398005bed80e0ac54f866b20 Mon Sep 17 00:00:00 2001
From: Gerhard Schlager
Date: Fri, 22 Jan 2021 16:03:43 +0100
Subject: [PATCH 22/85] UX: Makes the theme editor display placeholder
correctly for RTL languages (#11800)
This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-he#53834
---
.../admin/addon/components/ace-editor.js | 33 +++++++++++++++++++
.../addon/components/admin-theme-editor.js | 15 +++++++--
.../components/admin-theme-editor.hbs | 2 +-
.../discourse/app/lib/text-direction.js | 4 +++
app/assets/stylesheets/common/base/rtl.scss | 9 +++++
config/locales/client.en.yml | 4 +--
6 files changed, 60 insertions(+), 7 deletions(-)
diff --git a/app/assets/javascripts/admin/addon/components/ace-editor.js b/app/assets/javascripts/admin/addon/components/ace-editor.js
index 4f70345003..862d611188 100644
--- a/app/assets/javascripts/admin/addon/components/ace-editor.js
+++ b/app/assets/javascripts/admin/addon/components/ace-editor.js
@@ -10,6 +10,7 @@ export default Component.extend({
_editor: null,
_skipContentChangeEvent: null,
disabled: false,
+ htmlPlaceholder: false,
@observes("editorId")
editorIdChanged() {
@@ -86,6 +87,10 @@ export default Component.extend({
loadedAce.config.set("loadWorkerFromBlob", false);
loadedAce.config.set("workerPath", getURL("/javascripts/ace")); // Do not use CDN for workers
+ if (this.htmlPlaceholder) {
+ this._overridePlaceholder(loadedAce);
+ }
+
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
@@ -131,4 +136,32 @@ export default Component.extend({
}
},
},
+
+ _overridePlaceholder(loadedAce) {
+ const originalPlaceholderSetter =
+ loadedAce.config.$defaultOptions.editor.placeholder.set;
+
+ loadedAce.config.$defaultOptions.editor.placeholder.set = function () {
+ if (!this.$updatePlaceholder) {
+ const originalRendererOn = this.renderer.on;
+ this.renderer.on = function () {};
+ originalPlaceholderSetter.call(this, ...arguments);
+ this.renderer.on = originalRendererOn;
+
+ const originalUpdatePlaceholder = this.$updatePlaceholder;
+
+ this.$updatePlaceholder = function () {
+ originalUpdatePlaceholder.call(this, ...arguments);
+
+ if (this.renderer.placeholderNode) {
+ this.renderer.placeholderNode.innerHTML = this.$placeholder || "";
+ }
+ }.bind(this);
+
+ this.on("input", this.$updatePlaceholder);
+ }
+
+ this.$updatePlaceholder();
+ };
+ },
});
diff --git a/app/assets/javascripts/admin/addon/components/admin-theme-editor.js b/app/assets/javascripts/admin/addon/components/admin-theme-editor.js
index ac073c4b55..ebf733e749 100644
--- a/app/assets/javascripts/admin/addon/components/admin-theme-editor.js
+++ b/app/assets/javascripts/admin/addon/components/admin-theme-editor.js
@@ -2,6 +2,7 @@ import Component from "@ember/component";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed";
+import { isDocumentRTL } from "discourse/lib/text-direction";
import { next } from "@ember/runloop";
export default Component.extend({
@@ -43,9 +44,17 @@ export default Component.extend({
@discourseComputed("currentTargetName", "fieldName")
placeholder(targetName, fieldName) {
- return fieldName && fieldName === "color_definitions"
- ? I18n.t("admin.customize.theme.color_definitions.placeholder")
- : "";
+ if (fieldName && fieldName === "color_definitions") {
+ const example =
+ ":root {\n" +
+ " --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)};\n" +
+ "}";
+
+ return I18n.t("admin.customize.theme.color_definitions.placeholder", {
+ example: isDocumentRTL() ? `${example}
` : example,
+ });
+ }
+ return "";
},
@discourseComputed("fieldName", "currentTargetName", "theme")
diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs
index ce928108a2..5458ee10d8 100644
--- a/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs
+++ b/app/assets/javascripts/admin/addon/templates/components/admin-theme-editor.hbs
@@ -87,4 +87,4 @@
{{error}}
{{/if}}
-{{ace-editor content=activeSection editorId=editorId mode=activeSectionMode autofocus="true" placeholder=placeholder}}
+{{ace-editor content=activeSection editorId=editorId mode=activeSectionMode autofocus="true" placeholder=placeholder htmlPlaceholder=true}}
diff --git a/app/assets/javascripts/discourse/app/lib/text-direction.js b/app/assets/javascripts/discourse/app/lib/text-direction.js
index 013cbdaed4..b3da0964b6 100644
--- a/app/assets/javascripts/discourse/app/lib/text-direction.js
+++ b/app/assets/javascripts/discourse/app/lib/text-direction.js
@@ -29,3 +29,7 @@ export function siteDir() {
}
return _siteDir;
}
+
+export function isDocumentRTL() {
+ return siteDir() === "rtl";
+}
diff --git a/app/assets/stylesheets/common/base/rtl.scss b/app/assets/stylesheets/common/base/rtl.scss
index de3ac13d76..0995f19ca6 100644
--- a/app/assets/stylesheets/common/base/rtl.scss
+++ b/app/assets/stylesheets/common/base/rtl.scss
@@ -120,3 +120,12 @@
}
}
}
+
+.rtl .ace_placeholder {
+ direction: rtl !important;
+ text-align: right !important;
+
+ [dir="ltr"] {
+ text-align: left !important;
+ }
+}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index c5492d634c..330dcb1981 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -4166,9 +4166,7 @@ en:
Example:
- :root {
- --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)};
- }
+ %{example}
Prefixing the property names is highly recommended to avoid conflicts with plugins and/or core.
head_tag:
From 4f01ca87e30c4b7d63a71267e4a572eea786c152 Mon Sep 17 00:00:00 2001
From: Penar Musaraj
Date: Fri, 22 Jan 2021 10:09:02 -0500
Subject: [PATCH 23/85] FEATURE: Add new features section in admin dashboard
(#11731)
---
.../components/dashboard-new-features.js | 26 +++++++
.../admin/addon/models/admin-dashboard.js | 1 +
.../components/dashboard-new-feature-item.hbs | 13 ++++
.../components/dashboard-new-features.hbs | 21 ++++++
.../admin/addon/templates/dashboard.hbs | 2 +
.../tests/acceptance/dashboard-test.js | 7 ++
.../tests/fixtures/dashboard-new-features.js | 32 +++++++++
.../stylesheets/common/admin/dashboard.scss | 39 +++++++++++
app/controllers/admin/dashboard_controller.rb | 11 +++
config/locales/client.en.yml | 4 ++
config/routes.rb | 2 +
config/site_settings.yml | 4 ++
lib/discourse_updates.rb | 44 ++++++++++++
spec/components/discourse_updates_spec.rb | 68 +++++++++++++++++++
.../admin/dashboard_controller_spec.rb | 53 +++++++++++++++
15 files changed, 327 insertions(+)
create mode 100644 app/assets/javascripts/admin/addon/components/dashboard-new-features.js
create mode 100644 app/assets/javascripts/admin/addon/templates/components/dashboard-new-feature-item.hbs
create mode 100644 app/assets/javascripts/admin/addon/templates/components/dashboard-new-features.hbs
create mode 100644 app/assets/javascripts/discourse/tests/fixtures/dashboard-new-features.js
diff --git a/app/assets/javascripts/admin/addon/components/dashboard-new-features.js b/app/assets/javascripts/admin/addon/components/dashboard-new-features.js
new file mode 100644
index 0000000000..937340c1d3
--- /dev/null
+++ b/app/assets/javascripts/admin/addon/components/dashboard-new-features.js
@@ -0,0 +1,26 @@
+import Component from "@ember/component";
+import { action } from "@ember/object";
+import { ajax } from "discourse/lib/ajax";
+
+export default Component.extend({
+ newFeatures: null,
+ releaseNotesLink: null,
+
+ init() {
+ this._super(...arguments);
+
+ ajax("/admin/dashboard/new-features.json").then((json) => {
+ this.setProperties({
+ newFeatures: json.new_features,
+ releaseNotesLink: json.release_notes_link,
+ });
+ });
+ },
+
+ @action
+ dismissNewFeatures() {
+ ajax("/admin/dashboard/mark-new-features-as-seen.json", {
+ type: "PUT",
+ }).then(() => this.set("newFeatures", null));
+ },
+});
diff --git a/app/assets/javascripts/admin/addon/models/admin-dashboard.js b/app/assets/javascripts/admin/addon/models/admin-dashboard.js
index 400ebe161b..c0c9f86702 100644
--- a/app/assets/javascripts/admin/addon/models/admin-dashboard.js
+++ b/app/assets/javascripts/admin/addon/models/admin-dashboard.js
@@ -14,6 +14,7 @@ AdminDashboard.reopenClass({
return ajax("/admin/dashboard.json").then((json) => {
const model = AdminDashboard.create();
model.set("version_check", json.version_check);
+
return model;
});
},
diff --git a/app/assets/javascripts/admin/addon/templates/components/dashboard-new-feature-item.hbs b/app/assets/javascripts/admin/addon/templates/components/dashboard-new-feature-item.hbs
new file mode 100644
index 0000000000..2aff298640
--- /dev/null
+++ b/app/assets/javascripts/admin/addon/templates/components/dashboard-new-feature-item.hbs
@@ -0,0 +1,13 @@
+
+
{{item.emoji}}
+
+
+
{{item.description}}
+
+
diff --git a/app/assets/javascripts/admin/addon/templates/components/dashboard-new-features.hbs b/app/assets/javascripts/admin/addon/templates/components/dashboard-new-features.hbs
new file mode 100644
index 0000000000..190e53a432
--- /dev/null
+++ b/app/assets/javascripts/admin/addon/templates/components/dashboard-new-features.hbs
@@ -0,0 +1,21 @@
+{{#if newFeatures}}
+
+
+
{{replace-emoji (i18n "admin.dashboard.new_features.title") }}
+
+
+
+ {{#each newFeatures as |feature|}}
+ {{dashboard-new-feature-item item=feature}}
+ {{/each}}
+
+
+
+{{/if}}
diff --git a/app/assets/javascripts/admin/addon/templates/dashboard.hbs b/app/assets/javascripts/admin/addon/templates/dashboard.hbs
index 0e4f4beec2..cb87e84b47 100644
--- a/app/assets/javascripts/admin/addon/templates/dashboard.hbs
+++ b/app/assets/javascripts/admin/addon/templates/dashboard.hbs
@@ -1,3 +1,5 @@
+{{dashboard-new-features}}
+
{{plugin-outlet name="admin-dashboard-top"}}
{{#if showVersionChecks}}
diff --git a/app/assets/javascripts/discourse/tests/acceptance/dashboard-test.js b/app/assets/javascripts/discourse/tests/acceptance/dashboard-test.js
index b573383066..ac67b052df 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/dashboard-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/dashboard-test.js
@@ -127,6 +127,13 @@ acceptance("Dashboard", function (needs) {
"its set the value of the filter from the query params"
);
});
+
+ test("new features", async function (assert) {
+ await visit("/admin");
+
+ assert.ok(exists(".dashboard-new-features"));
+ assert.ok(exists(".dashboard-new-features .new-features-release-notes"));
+ });
});
acceptance("Dashboard: dashboard_visible_tabs", function (needs) {
diff --git a/app/assets/javascripts/discourse/tests/fixtures/dashboard-new-features.js b/app/assets/javascripts/discourse/tests/fixtures/dashboard-new-features.js
new file mode 100644
index 0000000000..fe9d50b791
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/fixtures/dashboard-new-features.js
@@ -0,0 +1,32 @@
+export default {
+ "/admin/dashboard/new-features.json": {
+ new_features: [
+ {
+ id: 1,
+ user_id: 127,
+ emoji: "😎",
+ title: "New color palettes",
+ description:
+ "New light and dark color palettes that adhere to Web Content Accessibility Guidelines. ",
+ tier: [],
+ link: "https://meta.discourse.org",
+ created_at: "2021-01-18T19:59:29.666Z",
+ updated_at: "2021-01-19T19:33:16.150Z",
+ },
+ {
+ id: 7,
+ user_id: 127,
+ emoji: "👱♀️",
+ title: "Suspend users quickly",
+ description:
+ "Staff can now suspend or silence a user immediately, without needing to visit the review queue or admin page. ",
+ tier: [],
+ link: "",
+ created_at: "2021-01-19T19:20:09.757Z",
+ updated_at: "2021-01-19T19:20:09.757Z",
+ }
+ ],
+ release_notes_link:
+ "https://meta.discourse.org/c/feature/announcements?tags=release-notes\u0026before=0",
+ },
+};
diff --git a/app/assets/stylesheets/common/admin/dashboard.scss b/app/assets/stylesheets/common/admin/dashboard.scss
index 8d38420b7b..0e553c4a9a 100644
--- a/app/assets/stylesheets/common/admin/dashboard.scss
+++ b/app/assets/stylesheets/common/admin/dashboard.scss
@@ -612,3 +612,42 @@
font-size: $font-up-3;
}
}
+
+.dashboard-new-features {
+ .section-body {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ grid-gap: 1.5em;
+ }
+
+ .section-footer {
+ margin: 1.5em;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ .btn {
+ margin-left: 1em;
+ }
+ }
+}
+
+.admin-new-feature-item {
+ display: flex;
+ align-items: flex-start;
+
+ .new-feature-emoji {
+ font-size: 3.5em;
+ padding-right: 0.5em;
+ padding-left: 0.5em;
+ }
+
+ .new-feature-content {
+ padding-right: 0.5em;
+ align-self: center;
+ .header {
+ font-size: $font-up-1;
+ font-weight: bold;
+ margin-bottom: 0.5em;
+ }
+ }
+}
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index eb246d75e5..1b75618f88 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -22,4 +22,15 @@ class Admin::DashboardController < Admin::AdminController
def problems
render_json_dump(problems: AdminDashboardData.fetch_problems(check_force_https: request.ssl?))
end
+
+ def new_features
+ data = { new_features: DiscourseUpdates.unseen_new_features(current_user.id) }
+ data.merge!(release_notes_link: AdminDashboardGeneralData.fetch_cached_stats["release_notes_link"])
+ render json: data
+ end
+
+ def mark_new_features_as_seen
+ DiscourseUpdates.mark_new_features_as_seen(current_user.id)
+ render json: success_json
+ end
end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 330dcb1981..30f998f7b3 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -3601,6 +3601,10 @@ en:
installed_version: "Installed"
latest_version: "Latest"
problems_found: "Some advice based on your current site settings"
+ new_features:
+ title: "🎁 New Features"
+ dismiss: "Dismiss"
+ learn_more: "Learn more"
last_checked: "Last checked"
refresh_problems: "Refresh"
no_problems: "No problems were found."
diff --git a/config/routes.rb b/config/routes.rb
index e30b0a7d2c..85c4092044 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -260,6 +260,8 @@ Discourse::Application.routes.draw do
get "dashboard/moderation" => "dashboard#moderation"
get "dashboard/security" => "dashboard#security"
get "dashboard/reports" => "dashboard#reports"
+ get "dashboard/new-features" => "dashboard#new_features"
+ put "dashboard/mark-new-features-as-seen" => "dashboard#mark_new_features_as_seen"
resources :dashboard, only: [:index] do
collection do
diff --git a/config/site_settings.yml b/config/site_settings.yml
index a64dba360c..d32b2ebc49 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -2134,6 +2134,10 @@ uncategorized:
client: true
hidden: true
+ check_for_new_features:
+ default: false
+ hidden: true
+
automatically_unpin_topics:
default: true
client: true
diff --git a/lib/discourse_updates.rb b/lib/discourse_updates.rb
index 6a7dc2a54b..d5d2f3b264 100644
--- a/lib/discourse_updates.rb
+++ b/lib/discourse_updates.rb
@@ -115,6 +115,38 @@ module DiscourseUpdates
keys.present? ? keys.map { |k| Discourse.redis.hgetall(k) } : []
end
+ def perform_new_feature_check
+ response = Excon.new(new_features_endpoint).request(expects: [200], method: :Get)
+ json = JSON.parse(response.body)
+ Discourse.redis.set(new_features_key, response.body)
+ end
+
+ def unseen_new_features(user_id)
+ entries = JSON.parse(Discourse.redis.get(new_features_key)) rescue nil
+ return nil if entries.nil?
+
+ last_seen = new_features_last_seen(user_id)
+
+ if last_seen.present?
+ entries.select! { |item| Time.zone.parse(item["created_at"]) > last_seen }
+ end
+
+ entries.sort { |item| Time.zone.parse(item["created_at"]) }
+ end
+
+ def new_features_last_seen(user_id)
+ last_seen = Discourse.redis.get new_features_last_seen_key(user_id)
+ return nil if last_seen.blank?
+ Time.zone.parse(last_seen)
+ end
+
+ def mark_new_features_as_seen(user_id)
+ entries = JSON.parse(Discourse.redis.get(new_features_key)) rescue nil
+ return nil if entries.nil?
+ last_seen = entries.max_by { |x| x["created_at"] }
+ Discourse.redis.set(new_features_last_seen_key(user_id), last_seen["created_at"])
+ end
+
private
def last_installed_version_key
@@ -144,5 +176,17 @@ module DiscourseUpdates
def missing_versions_key_prefix
'missing_version'
end
+
+ def new_features_endpoint
+ 'https://meta.discourse.org/new-features.json'
+ end
+
+ def new_features_key
+ 'new_features'
+ end
+
+ def new_features_last_seen_key(user_id)
+ "new_features_last_seen_user_#{user_id}"
+ end
end
end
diff --git a/spec/components/discourse_updates_spec.rb b/spec/components/discourse_updates_spec.rb
index 4b83510c24..e3f911ca6f 100644
--- a/spec/components/discourse_updates_spec.rb
+++ b/spec/components/discourse_updates_spec.rb
@@ -144,4 +144,72 @@ describe DiscourseUpdates do
include_examples "when last_installed_version is old"
end
end
+
+ context 'new features' do
+ fab!(:admin) { Fabricate(:admin) }
+ fab!(:admin2) { Fabricate(:admin) }
+ let!(:last_item_date) { 5.minutes.ago }
+ let!(:sample_features) { [
+ { "emoji" => "🤾", "title" => "Super Fruits", "description" => "Taste explosion!", "created_at" => 40.minutes.ago },
+ { "emoji" => "🙈", "title" => "Fancy Legumes", "description" => "Magic legumes!", "created_at" => 15.minutes.ago },
+ { "emoji" => "🤾", "title" => "Quality Veggies", "description" => "Green goodness!", "created_at" => last_item_date },
+ ] }
+
+ before(:each) do
+ Discourse.redis.del "new_features_last_seen_user_#{admin.id}"
+ Discourse.redis.del "new_features_last_seen_user_#{admin2.id}"
+ Discourse.redis.del "new_features"
+
+ Discourse.redis.set('new_features', MultiJson.dump(sample_features))
+ end
+
+ it 'returns all items on the first run' do
+ result = DiscourseUpdates.unseen_new_features(admin.id)
+
+ expect(result.length).to eq(3)
+ expect(result[2]["title"]).to eq("Super Fruits")
+ end
+
+ it 'returns only unseen items by user' do
+ DiscourseUpdates.stubs(:new_features_last_seen).with(admin.id).returns(10.minutes.ago)
+ DiscourseUpdates.stubs(:new_features_last_seen).with(admin2.id).returns(30.minutes.ago)
+
+ result = DiscourseUpdates.unseen_new_features(admin.id)
+ expect(result.length).to eq(1)
+ expect(result[0]["title"]).to eq("Quality Veggies")
+
+ result2 = DiscourseUpdates.unseen_new_features(admin2.id)
+ expect(result2.length).to eq(2)
+ expect(result2[0]["title"]).to eq("Quality Veggies")
+ expect(result2[1]["title"]).to eq("Fancy Legumes")
+ end
+
+ it 'can mark features as seen for a given user' do
+ expect(DiscourseUpdates.unseen_new_features(admin.id)).to be_present
+
+ DiscourseUpdates.mark_new_features_as_seen(admin.id)
+ expect(DiscourseUpdates.unseen_new_features(admin.id)).to be_empty
+
+ # doesn't affect another user
+ expect(DiscourseUpdates.unseen_new_features(admin2.id)).to be_present
+
+ end
+
+ it 'correctly sees newly added features as unseen' do
+ DiscourseUpdates.mark_new_features_as_seen(admin.id)
+ expect(DiscourseUpdates.unseen_new_features(admin.id)).to be_empty
+ expect(DiscourseUpdates.new_features_last_seen(admin.id)).to be_within(1.second).of (last_item_date)
+
+ updated_features = [
+ { "emoji" => "🤾", "title" => "Brand New Item", "created_at" => 2.minutes.ago }
+ ]
+ updated_features += sample_features
+
+ Discourse.redis.set('new_features', MultiJson.dump(updated_features))
+
+ result = DiscourseUpdates.unseen_new_features(admin.id)
+ expect(result.length).to eq(1)
+ expect(result[0]["title"]).to eq("Brand New Item")
+ end
+ end
end
diff --git a/spec/requests/admin/dashboard_controller_spec.rb b/spec/requests/admin/dashboard_controller_spec.rb
index 46602ae91d..b838f169dc 100644
--- a/spec/requests/admin/dashboard_controller_spec.rb
+++ b/spec/requests/admin/dashboard_controller_spec.rb
@@ -15,6 +15,15 @@ describe Admin::DashboardController do
context 'while logged in as an admin' do
fab!(:admin) { Fabricate(:admin) }
+ def populate_new_features
+ sample_features = [
+ { "id" => "1", "emoji" => "🤾", "title" => "Cool Beans", "description" => "Now beans are included", "created_at" => Time.zone.now - 40.minutes },
+ { "id" => "2", "emoji" => "🙈", "title" => "Fancy Legumes", "description" => "Legumes too!", "created_at" => Time.zone.now - 20.minutes }
+ ]
+
+ Discourse.redis.set('new_features', MultiJson.dump(sample_features))
+ end
+
before do
sign_in(admin)
end
@@ -77,5 +86,49 @@ describe Admin::DashboardController do
end
end
end
+
+ describe '#new_features' do
+ before do
+ Discourse.redis.del "new_features_last_seen_user_#{admin.id}"
+ Discourse.redis.del "new_features"
+ end
+
+ it 'is empty by default' do
+ get "/admin/dashboard/new-features.json"
+ expect(response.status).to eq(200)
+ json = response.parsed_body
+ expect(json['new_features']).to eq(nil)
+ end
+
+ it 'fails gracefully for invalid JSON' do
+ Discourse.redis.set("new_features", "INVALID JSON")
+ get "/admin/dashboard/new-features.json"
+ expect(response.status).to eq(200)
+ json = response.parsed_body
+ expect(json['new_features']).to eq(nil)
+ end
+
+ it 'includes new features when available' do
+ populate_new_features
+
+ get "/admin/dashboard/new-features.json"
+ expect(response.status).to eq(200)
+ json = response.parsed_body
+
+ expect(json['new_features'].length).to eq(2)
+ expect(json['new_features'][0]["emoji"]).to eq("🙈")
+ expect(json['new_features'][0]["title"]).to eq("Fancy Legumes")
+ end
+ end
+
+ describe '#mark_new_features_as_seen' do
+ it 'resets last seen for a given user' do
+ populate_new_features
+ put "/admin/dashboard/mark-new-features-as-seen.json"
+
+ expect(response.status).to eq(200)
+ expect(DiscourseUpdates.new_features_last_seen(admin.id)).not_to eq(nil)
+ end
+ end
end
end
From dc268822a405a23c3dc59a5537d3b72d00d7876f Mon Sep 17 00:00:00 2001
From: Robin Ward
Date: Fri, 22 Jan 2021 10:41:01 -0500
Subject: [PATCH 24/85] FIX: It seems sometimes shims are evaluated by older JS
engines (#11813)
This gives us backwards compatibility with those.
---
app/assets/javascripts/handlebars-shim.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/assets/javascripts/handlebars-shim.js b/app/assets/javascripts/handlebars-shim.js
index d73b33ce45..9d8403d189 100644
--- a/app/assets/javascripts/handlebars-shim.js
+++ b/app/assets/javascripts/handlebars-shim.js
@@ -7,7 +7,7 @@ if (typeof define !== "undefined") {
__exports__.default = Handlebars;
__exports__.compile = function () {
// eslint-disable-next-line
- return Handlebars.compile(...arguments);
+ return Handlebars.compile.apply(this, arguments);
};
}
});
From 6f13d2b03912165e2d9d5830a2ef6f73b535f3f4 Mon Sep 17 00:00:00 2001
From: Joffrey JAFFEUX
Date: Fri, 22 Jan 2021 17:09:39 +0100
Subject: [PATCH 25/85] A11Y: makes post-edits-indicator a button instead of a
link (#11811)
---
.../discourse/app/widgets/button.js | 9 +++++++
.../app/widgets/post-edits-indicator.js | 26 ++++++-------------
.../discourse/tests/acceptance/topic-test.js | 2 +-
.../discourse/tests/helpers/qunit-helpers.js | 4 +++
.../tests/integration/widgets/button-test.js | 13 ++++++++++
.../tests/integration/widgets/post-test.js | 2 +-
.../stylesheets/common/base/_topic-list.scss | 14 +++++++---
.../stylesheets/common/base/topic-post.scss | 15 +++++++++++
8 files changed, 62 insertions(+), 23 deletions(-)
diff --git a/app/assets/javascripts/discourse/app/widgets/button.js b/app/assets/javascripts/discourse/app/widgets/button.js
index 413d01ba28..bb93acdbb5 100644
--- a/app/assets/javascripts/discourse/app/widgets/button.js
+++ b/app/assets/javascripts/discourse/app/widgets/button.js
@@ -62,6 +62,15 @@ export const ButtonClass = {
h("span.d-button-label", I18n.t(attrs.label, attrs.labelOptions))
);
}
+ if (attrs.translatedLabel) {
+ contents.push(
+ h(
+ "span.d-button-label",
+ attrs.translatedLabel.toString(),
+ attrs.translatedLabelOptions
+ )
+ );
+ }
if (attrs.contents) {
contents.push(attrs.contents);
}
diff --git a/app/assets/javascripts/discourse/app/widgets/post-edits-indicator.js b/app/assets/javascripts/discourse/app/widgets/post-edits-indicator.js
index 818da0e0c3..17bf30302b 100644
--- a/app/assets/javascripts/discourse/app/widgets/post-edits-indicator.js
+++ b/app/assets/javascripts/discourse/app/widgets/post-edits-indicator.js
@@ -1,7 +1,5 @@
import I18n from "I18n";
import { createWidget } from "discourse/widgets/widget";
-import { h } from "virtual-dom";
-import { iconNode } from "discourse-common/lib/icon-library";
import { longDate } from "discourse/lib/formatter";
function mult(val) {
@@ -53,24 +51,16 @@ export default createWidget("post-edits-indicator", {
title = `${I18n.t("post.last_edited_on")} ${date}`;
}
- const contents = [
- attrs.version > 1 ? attrs.version - 1 : "",
- " ",
- iconNode(icon),
- ];
-
- return h(
- "a",
- {
- className,
- attributes: { title, href: "#" },
- },
- contents
- );
+ return this.attach("flat-button", {
+ icon,
+ title,
+ className,
+ action: "onPostEditsIndicatorClick",
+ translatedLabel: attrs.version > 1 ? attrs.version - 1 : "",
+ });
},
- click(e) {
- e.preventDefault();
+ onPostEditsIndicatorClick() {
if (this.attrs.wiki && this.attrs.version === 1) {
this.sendWidgetAction("editPost");
} else if (this.attrs.canViewEditHistory) {
diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-test.js
index 845d58e45d..29b8d6d8c2 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/topic-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/topic-test.js
@@ -146,7 +146,7 @@ acceptance("Topic", function (needs) {
await click(".topic-post:nth-of-type(1) button.show-post-admin-menu");
await click(".btn.wiki");
- assert.ok(queryAll("a.wiki").length === 1, "it shows the wiki icon");
+ assert.ok(queryAll("button.wiki").length === 1, "it shows the wiki icon");
});
test("Visit topic routes", async function (assert) {
diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js
index 7072dbea9a..948abcd31c 100644
--- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js
+++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js
@@ -378,6 +378,10 @@ export function queryAll() {
return window.find(...arguments);
}
+export function query() {
+ return document.querySelector(...arguments);
+}
+
export function invisible(selector) {
const $items = queryAll(selector + ":visible");
return (
diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/button-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/button-test.js
index 343aeb729e..e1aa1be23d 100644
--- a/app/assets/javascripts/discourse/tests/integration/widgets/button-test.js
+++ b/app/assets/javascripts/discourse/tests/integration/widgets/button-test.js
@@ -3,6 +3,7 @@ import componentTest, {
} from "discourse/tests/helpers/component-test";
import {
discourseModule,
+ query,
queryAll,
} from "discourse/tests/helpers/qunit-helpers";
@@ -69,4 +70,16 @@ discourseModule("Integration | Component | Widget | button", function (hooks) {
);
},
});
+
+ componentTest("translatedLabel", {
+ template: '{{mount-widget widget="button" args=args}}',
+
+ beforeEach() {
+ this.set("args", { translatedLabel: "foo bar" });
+ },
+
+ test(assert) {
+ assert.equal(query("button span.d-button-label").innerText, "foo bar");
+ },
+ });
});
diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/post-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/post-test.js
index 9206c230eb..5f74973677 100644
--- a/app/assets/javascripts/discourse/tests/integration/widgets/post-test.js
+++ b/app/assets/javascripts/discourse/tests/integration/widgets/post-test.js
@@ -113,7 +113,7 @@ discourseModule("Integration | Component | Widget | post", function (hooks) {
this.on("showHistory", () => (this.historyShown = true));
},
async test(assert) {
- await click(".post-info.edits");
+ await click(".post-info.edits button");
assert.ok(this.historyShown, "clicking the pencil shows the history");
},
});
diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss
index d362f9876b..81b1155a59 100644
--- a/app/assets/stylesheets/common/base/_topic-list.scss
+++ b/app/assets/stylesheets/common/base/_topic-list.scss
@@ -295,15 +295,23 @@
}
.heatmap-high,
-.heatmap-high a {
+.heatmap-high a,
+.heatmap-high .d-icon,
+.heatmap-high .d-button-label {
color: #fe7a15 !important;
}
+
.heatmap-med,
-.heatmap-med a {
+.heatmap-med a,
+.heatmap-med .d-icon,
+.heatmap-med .d-button-label {
color: #cf7721 !important;
}
+
.heatmap-low,
-.heatmap-low a {
+.heatmap-low a,
+.heatmap-low .d-icon,
+.heatmap-low .d-button-label {
color: #9b764f !important;
}
diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss
index 67eb33a49a..df0c0c9065 100644
--- a/app/assets/stylesheets/common/base/topic-post.scss
+++ b/app/assets/stylesheets/common/base/topic-post.scss
@@ -655,6 +655,21 @@ blockquote {
&.raw-email {
cursor: pointer;
}
+ &.edits {
+ .widget-button {
+ display: flex;
+ align-items: center;
+
+ .d-button-label {
+ order: 0;
+ padding-right: 0.25em;
+ }
+
+ .d-icon {
+ order: 1;
+ }
+ }
+ }
}
pre {
From 56294b4fba9a850d8a62ab0ab19e581fff243b95 Mon Sep 17 00:00:00 2001
From: Mark VanLandingham
Date: Fri, 22 Jan 2021 13:02:11 -0600
Subject: [PATCH 26/85] FIX: Remove scheduled DND timings when schedule is
disabed (#11814)
---
app/models/user_notification_schedule.rb | 6 +++++-
app/services/user_updater.rb | 4 +++-
spec/services/user_updater_spec.rb | 12 ++++++++++++
3 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/app/models/user_notification_schedule.rb b/app/models/user_notification_schedule.rb
index 18892506db..f2346d4e17 100644
--- a/app/models/user_notification_schedule.rb
+++ b/app/models/user_notification_schedule.rb
@@ -18,10 +18,14 @@ class UserNotificationSchedule < ActiveRecord::Base
scope :enabled, -> { where(enabled: true) }
def create_do_not_disturb_timings(delete_existing: false)
- user.do_not_disturb_timings.where(scheduled: true).destroy_all if delete_existing
+ destroy_scheduled_timings if delete_existing
UserNotificationScheduleProcessor.create_do_not_disturb_timings_for(self)
end
+ def destroy_scheduled_timings
+ user.do_not_disturb_timings.where(scheduled: true).destroy_all
+ end
+
private
def has_valid_times
diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb
index 41593b84db..544b9883ca 100644
--- a/app/services/user_updater.rb
+++ b/app/services/user_updater.rb
@@ -200,7 +200,9 @@ class UserUpdater
if saved
if user_notification_schedule
- user_notification_schedule.create_do_not_disturb_timings(delete_existing: true)
+ user_notification_schedule.enabled ?
+ user_notification_schedule.create_do_not_disturb_timings(delete_existing: true) :
+ user_notification_schedule.destroy_scheduled_timings
end
DiscourseEvent.trigger(:user_updated, user)
end
diff --git a/spec/services/user_updater_spec.rb b/spec/services/user_updater_spec.rb
index abbaadf935..7ed290999e 100644
--- a/spec/services/user_updater_spec.rb
+++ b/spec/services/user_updater_spec.rb
@@ -251,6 +251,18 @@ describe UserUpdater do
updater.update(user_notification_schedule: schedule_attrs)
}.to change { user.do_not_disturb_timings.count }.by(4)
end
+
+ it "removes do_not_disturb_timings when the schedule is disabled" do
+ updater = UserUpdater.new(acting_user, user)
+ updater.update(user_notification_schedule: schedule_attrs)
+ expect(user.user_notification_schedule.enabled).to eq(true)
+
+ schedule_attrs[:enabled] = false
+ updater.update(user_notification_schedule: schedule_attrs)
+
+ expect(user.user_notification_schedule.enabled).to eq(false)
+ expect(user.do_not_disturb_timings.count).to eq(0)
+ end
end
context 'when sso overrides bio' do
From cd11689446e72b9c1f3f10551fead2780cc310b9 Mon Sep 17 00:00:00 2001
From: David Taylor
Date: Fri, 22 Jan 2021 19:16:43 +0000
Subject: [PATCH 27/85] FIX: Check the confirmation result before deleting SSO
record (#11816)
---
.../javascripts/admin/addon/controllers/admin-user-index.js | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/app/assets/javascripts/admin/addon/controllers/admin-user-index.js b/app/assets/javascripts/admin/addon/controllers/admin-user-index.js
index 81a42481e2..bfecfd877b 100644
--- a/app/assets/javascripts/admin/addon/controllers/admin-user-index.js
+++ b/app/assets/javascripts/admin/addon/controllers/admin-user-index.js
@@ -601,8 +601,10 @@ export default Controller.extend(CanCheckEmails, {
I18n.t("admin.user.sso.confirm_delete"),
I18n.t("no_value"),
I18n.t("yes_value"),
- () => {
- return this.model.deleteSSORecord();
+ (confirmed) => {
+ if (confirmed) {
+ return this.model.deleteSSORecord();
+ }
}
);
},
From 73cb083b7b0a98f89a510be4b62bcef6b0bc2b98 Mon Sep 17 00:00:00 2001
From: Penar Musaraj
Date: Fri, 22 Jan 2021 16:27:19 -0500
Subject: [PATCH 28/85] FIX: Cannot find currentThemeColorSchemeId when no
themeId is present (#11817)
---
.../discourse/app/controllers/preferences/interface.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/interface.js b/app/assets/javascripts/discourse/app/controllers/preferences/interface.js
index aff0e805b8..2ae8253e64 100644
--- a/app/assets/javascripts/discourse/app/controllers/preferences/interface.js
+++ b/app/assets/javascripts/discourse/app/controllers/preferences/interface.js
@@ -153,7 +153,7 @@ export default Controller.extend({
"themeId"
)
currentSchemeCanBeSelected(userThemes, userColorSchemes, themeId) {
- if (!userThemes) {
+ if (!userThemes || !themeId) {
return false;
}
From 6d30e01d1c6851c978982e2611b38a91b5617e8e Mon Sep 17 00:00:00 2001
From: Roman Rizzi
Date: Fri, 22 Jan 2021 19:05:14 -0300
Subject: [PATCH 29/85] A11Y: Structure user menu as tabs. (#11789)
* A11Y: Structure user menu as tabs.
Although the user menu content has the appearance of tabs and relies on the functionality of tabs to make sense in terms of content and focus order, it is not marked up correctly as tabs and tab panels. See [WAI-ARIA Authoring Practices 1.1](https://www.w3.org/TR/wai-aria-practices-1.1/#tabpanel) and the [example](https://www.w3.org/TR/wai-aria-practices-1.1/examples/tabs/tabs-2/tabs.html) for details.
* Make plugin api backwards compatible
---
.../discourse/app/lib/plugin-api.js | 4 +-
.../discourse/app/widgets/button.js | 24 +++++-
.../app/widgets/quick-access-panel.js | 9 ++
.../discourse/app/widgets/user-menu.js | 67 +++++++++++----
.../integration/widgets/user-menu-test.js | 10 ++-
.../stylesheets/common/base/menu-panel.scss | 85 +++++++++----------
6 files changed, 132 insertions(+), 67 deletions(-)
diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js
index b536e80dbb..ce4145cd5e 100644
--- a/app/assets/javascripts/discourse/app/lib/plugin-api.js
+++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js
@@ -737,10 +737,10 @@ class PluginApi {
* example:
*
* api.addUserMenuGlyph({
- * label: 'awesome.label',
+ * title: 'awesome.label',
* className: 'my-class',
* icon: 'my-icon',
- * href: `/some/path`
+ * data: { url: `/some/path` },
* });
*
*/
diff --git a/app/assets/javascripts/discourse/app/widgets/button.js b/app/assets/javascripts/discourse/app/widgets/button.js
index bb93acdbb5..fe5c6d93ae 100644
--- a/app/assets/javascripts/discourse/app/widgets/button.js
+++ b/app/assets/javascripts/discourse/app/widgets/button.js
@@ -38,6 +38,17 @@ export const ButtonClass = {
attributes.title = title;
}
+ if (attrs.role) {
+ attributes["role"] = attrs.role;
+ }
+
+ if (attrs.tabAttrs) {
+ const tab = attrs.tabAttrs;
+ attributes["aria-selected"] = tab["aria-selected"];
+ attributes["tabindex"] = tab["tabindex"];
+ attributes["aria-controls"] = tab["aria-controls"];
+ }
+
if (attrs.disabled) {
attributes.disabled = "true";
}
@@ -51,11 +62,20 @@ export const ButtonClass = {
return attributes;
},
+ _buildIcon(attrs) {
+ const icon = iconNode(attrs.icon, { class: attrs.iconClass });
+ if (attrs["aria-label"]) {
+ icon.properties.attributes["role"] = "img";
+ icon.properties.attributes["aria-hidden"] = false;
+ }
+ return icon;
+ },
+
html(attrs) {
const contents = [];
const left = !attrs.iconRight;
if (attrs.icon && left) {
- contents.push(iconNode(attrs.icon, { class: attrs.iconClass }));
+ contents.push(this._buildIcon(attrs));
}
if (attrs.label) {
contents.push(
@@ -75,7 +95,7 @@ export const ButtonClass = {
contents.push(attrs.contents);
}
if (attrs.icon && !left) {
- contents.push(iconNode(attrs.icon, { class: attrs.iconClass }));
+ contents.push(this._buildIcon(attrs));
}
return contents;
diff --git a/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js b/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js
index 23145723b5..8c6f0996f2 100644
--- a/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js
+++ b/app/assets/javascripts/discourse/app/widgets/quick-access-panel.js
@@ -40,6 +40,15 @@ export default createWidget("quick-access-panel", {
return Promise.resolve([]);
},
+ buildAttributes() {
+ const attributes = this.attrs;
+ attributes["aria-labelledby"] = this.key;
+ attributes["tabindex"] = "0";
+ attributes["role"] = "tabpanel";
+
+ return attributes;
+ },
+
newItemsLoaded() {},
itemHtml(item) {}, // eslint-disable-line no-unused-vars
diff --git a/app/assets/javascripts/discourse/app/widgets/user-menu.js b/app/assets/javascripts/discourse/app/widgets/user-menu.js
index e532b38a21..7c89f3da0c 100644
--- a/app/assets/javascripts/discourse/app/widgets/user-menu.js
+++ b/app/assets/javascripts/discourse/app/widgets/user-menu.js
@@ -23,51 +23,82 @@ export function addUserMenuGlyph(glyph) {
createWidget("user-menu-links", {
tagName: "div.menu-links-header",
+ _tabAttrs(quickAccessType) {
+ return {
+ "aria-controls": `quick-access-${quickAccessType}`,
+ "aria-selected": "false",
+ tabindex: "-1",
+ };
+ },
+
+ // TODO: Remove when 2.7 gets released.
+ _structureAsTab(extraGlyph) {
+ const glyph = extraGlyph;
+ // Assume glyph is a button if it has a data-url field.
+ if (!glyph.data || !glyph.data.url) {
+ glyph.title = glyph.label;
+ glyph.data = { url: glyph.href };
+
+ glyph.label = null;
+ glyph.href = null;
+ }
+
+ glyph.role = "tab";
+ glyph.tabAttrs = this._tabAttrs(glyph.actionParam);
+
+ return glyph;
+ },
+
profileGlyph() {
return {
- label: "user.preferences",
+ title: "user.preferences",
className: "user-preferences-link",
icon: "user",
- href: `${this.attrs.path}/summary`,
action: UserMenuAction.QUICK_ACCESS,
actionParam: QuickAccess.PROFILE,
- "aria-label": "user.preferences",
+ data: { url: `${this.attrs.path}/summary` },
+ role: "tab",
+ tabAttrs: this._tabAttrs(QuickAccess.PROFILE),
};
},
notificationsGlyph() {
return {
- label: "user.notifications",
+ title: "user.notifications",
className: "user-notifications-link",
icon: "bell",
- href: `${this.attrs.path}/notifications`,
action: UserMenuAction.QUICK_ACCESS,
actionParam: QuickAccess.NOTIFICATIONS,
- "aria-label": "user.notifications",
+ data: { url: `${this.attrs.path}/notifications` },
+ role: "tab",
+ tabAttrs: this._tabAttrs(QuickAccess.NOTIFICATIONS),
};
},
bookmarksGlyph() {
return {
+ title: "user.bookmarks",
action: UserMenuAction.QUICK_ACCESS,
actionParam: QuickAccess.BOOKMARKS,
- label: "user.bookmarks",
className: "user-bookmarks-link",
icon: "bookmark",
- href: `${this.attrs.path}/activity/bookmarks`,
+ data: { url: `${this.attrs.path}/activity/bookmarks` },
"aria-label": "user.bookmarks",
+ role: "tab",
+ tabAttrs: this._tabAttrs(QuickAccess.BOOKMARKS),
};
},
messagesGlyph() {
return {
+ title: "user.private_messages",
action: UserMenuAction.QUICK_ACCESS,
actionParam: QuickAccess.MESSAGES,
- label: "user.private_messages",
className: "user-pms-link",
icon: "envelope",
- href: `${this.attrs.path}/messages`,
- "aria-label": "user.private_messages",
+ data: { url: `${this.attrs.path}/messages` },
+ role: "tab",
+ tabAttrs: this._tabAttrs(QuickAccess.MESSAGES),
};
},
@@ -82,7 +113,7 @@ createWidget("user-menu-links", {
if (this.isActive(glyph)) {
glyph = this.markAsActive(glyph);
}
- return this.attach("link", $.extend(glyph, { hideLabel: true }));
+ return this.attach("flat-button", glyph);
},
html() {
@@ -94,7 +125,8 @@ createWidget("user-menu-links", {
g = g(this);
}
if (g) {
- glyphs.push(g);
+ const structuredGlyph = this._structureAsTab(g);
+ glyphs.push(structuredGlyph);
}
});
}
@@ -108,9 +140,10 @@ createWidget("user-menu-links", {
glyphs.push(this.profileGlyph());
- return h("ul.menu-links-row", [
+ return h("div.menu-links-row", [
h(
- "li.glyphs",
+ "div.glyphs",
+ { attributes: { "aria-label": "Menu links", role: "tablist" } },
glyphs.map((l) => this.glyphHtml(l))
),
]);
@@ -121,6 +154,7 @@ createWidget("user-menu-links", {
// the full page.
definition.action = null;
definition.actionParam = null;
+ definition.url = definition.data.url;
if (definition.className) {
definition.className += " active";
@@ -128,6 +162,9 @@ createWidget("user-menu-links", {
definition.className = "active";
}
+ definition.tabAttrs["tabindex"] = "0";
+ definition.tabAttrs["aria-selected"] = "true";
+
return definition;
},
diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/user-menu-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/user-menu-test.js
index 891c07101c..afe1ad3bf6 100644
--- a/app/assets/javascripts/discourse/tests/integration/widgets/user-menu-test.js
+++ b/app/assets/javascripts/discourse/tests/integration/widgets/user-menu-test.js
@@ -79,7 +79,9 @@ discourseModule("Integration | Component | Widget | user-menu", function (
const routeToStub = sinon.stub(DiscourseURL, "routeTo");
await click(".user-notifications-link");
assert.ok(
- routeToStub.calledWith(queryAll(".user-notifications-link")[0].href),
+ routeToStub.calledWith(
+ queryAll(".user-notifications-link").data("url")
+ ),
"a second click should redirect to the full notifications page"
);
},
@@ -120,7 +122,7 @@ discourseModule("Integration | Component | Widget | user-menu", function (
},
async test(assert) {
- const userPmsLink = queryAll(".user-pms-link")[0];
+ const userPmsLink = queryAll(".user-pms-link").data("url");
assert.ok(userPmsLink);
await click(".user-pms-link");
@@ -143,7 +145,7 @@ discourseModule("Integration | Component | Widget | user-menu", function (
const routeToStub = sinon.stub(DiscourseURL, "routeTo");
await click(".user-pms-link");
assert.ok(
- routeToStub.calledWith(userPmsLink.href),
+ routeToStub.calledWith(userPmsLink),
"a second click should redirect to the full private messages page"
);
},
@@ -171,7 +173,7 @@ discourseModule("Integration | Component | Widget | user-menu", function (
const routeToStub = sinon.stub(DiscourseURL, "routeTo");
await click(".user-bookmarks-link");
assert.ok(
- routeToStub.calledWith(queryAll(".user-bookmarks-link")[0].href),
+ routeToStub.calledWith(queryAll(".user-bookmarks-link").data("url")),
"a second click should redirect to the full bookmarks page"
);
},
diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss
index 9d93eb26f8..2fde6944ae 100644
--- a/app/assets/stylesheets/common/base/menu-panel.scss
+++ b/app/assets/stylesheets/common/base/menu-panel.scss
@@ -380,76 +380,73 @@ div.menu-links-header {
display: flex;
width: 100%;
z-index: 2;
+ justify-content: space-between;
- li {
+ .glyphs {
display: inline-flex;
align-items: center;
- flex-wrap: wrap;
+ flex-wrap: nowrap;
+ width: 100%;
+ justify-content: space-between;
+ padding: 0;
- &.glyphs {
- flex-wrap: nowrap;
- width: 100%;
- justify-content: space-between;
- padding: 0;
-
- a {
- display: flex;
- flex: 1 1 auto;
- padding: 0.65em 0.25em 0.75em;
- justify-content: center;
- }
- }
-
- a,
button {
- // This is to make sure active and inactive tab icons have the same
- // size. `box-sizing` does not work and I have no idea why.
- border: 1px solid transparent;
- &:not(.active):hover {
- border-bottom: 0;
- margin-top: -1px;
- }
+ display: flex;
+ flex: 1 1 auto;
+ padding: 0.65em 0.25em 0.75em;
+ justify-content: center;
+ }
+ }
+
+ button {
+ // This is to make sure active and inactive tab icons have the same
+ // size. `box-sizing` does not work and I have no idea why.
+ border: 1px solid transparent;
+ &:not(.active):hover {
+ border-bottom: 0;
+ margin-top: -1px;
+ }
+ }
+
+ button.active {
+ border: 1px solid var(--primary-low);
+ border-bottom: 1px solid var(--secondary);
+ position: relative;
+
+ .d-icon {
+ color: var(--primary-high);
}
- a.active {
- border: 1px solid var(--primary-low);
- border-bottom: 1px solid var(--secondary);
- position: relative;
-
- .d-icon {
- color: var(--primary-high);
- }
-
- &:focus,
- &:hover {
- background-color: inherit;
- }
+ &:focus,
+ &:hover {
+ background-color: inherit;
}
}
}
- a:hover,
- a:focus {
+
+ button:hover,
+ button:focus {
background-color: var(--highlight-medium);
outline: none;
}
- a {
+ button {
padding: 0.3em 0.5em;
}
- li {
+ .glyphs {
display: table-cell;
width: auto;
text-align: center;
}
- li:first-child {
+ .glyphs:first-child {
text-align: left;
}
- li:last-child {
+ .glyphs:last-child {
text-align: right;
}
.fa,
- a {
+ button {
color: var(--primary-med-or-secondary-med);
}
}
From 17e683d373320f94e63c0e21c668c605b7bcd9d6 Mon Sep 17 00:00:00 2001
From: tshenry
Date: Fri, 22 Jan 2021 17:21:26 -0800
Subject: [PATCH 30/85] UX: Simplify narrative bot bio (#11820)
---
plugins/discourse-narrative-bot/config/locales/server.en.yml | 2 +-
plugins/discourse-narrative-bot/db/fixtures/001_discobot.rb | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/plugins/discourse-narrative-bot/config/locales/server.en.yml b/plugins/discourse-narrative-bot/config/locales/server.en.yml
index 750201811e..2d4887f4be 100644
--- a/plugins/discourse-narrative-bot/config/locales/server.en.yml
+++ b/plugins/discourse-narrative-bot/config/locales/server.en.yml
@@ -21,7 +21,7 @@ en:
This badge is granted upon successful completion of the interactive advanced user tutorial. You’ve mastered the advanced tools of discussion — and now you’re fully licensed!
discourse_narrative_bot:
- bio: "Hi, I’m not a real person. I’m a bot that can teach you about this site. To interact with me, send me a message or mention **`@%{discobot_username}`** anywhere."
+ bio: "Hi, I’m not a real person. I’m a bot that can teach you about this site. To interact with me, send me a message or mention me by name."
tl2_promotion_message:
subject_template: "Now that you’ve been promoted, it’s time to learn about some advanced features!"
diff --git a/plugins/discourse-narrative-bot/db/fixtures/001_discobot.rb b/plugins/discourse-narrative-bot/db/fixtures/001_discobot.rb
index 527dfbe3c9..9586bc483f 100644
--- a/plugins/discourse-narrative-bot/db/fixtures/001_discobot.rb
+++ b/plugins/discourse-narrative-bot/db/fixtures/001_discobot.rb
@@ -49,7 +49,7 @@ bot.create_user_profile! if !bot.user_profile
if !bot.user_profile.bio_raw
bot.user_profile.update!(
- bio_raw: I18n.t('discourse_narrative_bot.bio', site_title: SiteSetting.title, discobot_username: bot.username)
+ bio_raw: I18n.t('discourse_narrative_bot.bio')
)
end
From 37f7f306400c809406da2743b949b8e1e43e6572 Mon Sep 17 00:00:00 2001
From: Kris
Date: Fri, 22 Jan 2021 20:31:01 -0500
Subject: [PATCH 31/85] edit button fix, follow up to 6f13d2b (#11821)
---
app/assets/stylesheets/common/base/topic-post.scss | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss
index df0c0c9065..5ff2d73e88 100644
--- a/app/assets/stylesheets/common/base/topic-post.scss
+++ b/app/assets/stylesheets/common/base/topic-post.scss
@@ -663,10 +663,23 @@ blockquote {
.d-button-label {
order: 0;
padding-right: 0.25em;
+ color: var(--primary-med-or-secondary-med);
+ transition: color 0.25s;
}
.d-icon {
order: 1;
+ transition: color 0.25s;
+ }
+ .discourse-no-touch & {
+ &:hover {
+ .d-button-label {
+ color: var(--primary-high);
+ }
+ .d-icon {
+ color: var(--primary-high);
+ }
+ }
}
}
}
From 2ab5637792669855b0d04ea665f55b3c285aff70 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 24 Jan 2021 22:44:45 +0100
Subject: [PATCH 32/85] Build(deps-dev): Bump shoulda-matchers from 4.5.0 to
4.5.1 (#11826)
Bumps [shoulda-matchers](https://github.com/thoughtbot/shoulda-matchers) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/thoughtbot/shoulda-matchers/releases)
- [Changelog](https://github.com/thoughtbot/shoulda-matchers/blob/master/CHANGELOG.md)
- [Commits](https://github.com/thoughtbot/shoulda-matchers/compare/v4.5.0...v4.5.1)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Gemfile.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 9f569692c8..0fe450d4ff 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -403,7 +403,7 @@ GEM
seed-fu (2.3.9)
activerecord (>= 3.1)
activesupport (>= 3.1)
- shoulda-matchers (4.5.0)
+ shoulda-matchers (4.5.1)
activesupport (>= 4.2.0)
sidekiq (6.1.3)
connection_pool (>= 2.2.2)
From 831e29873df1edafe72d9fd69413cc0a07d32755 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 24 Jan 2021 22:45:44 +0100
Subject: [PATCH 33/85] Build(deps): Bump ast from 2.4.1 to 2.4.2 (#11825)
Bumps [ast](https://github.com/whitequark/ast) from 2.4.1 to 2.4.2.
- [Release notes](https://github.com/whitequark/ast/releases)
- [Changelog](https://github.com/whitequark/ast/blob/master/CHANGELOG.md)
- [Commits](https://github.com/whitequark/ast/compare/v2.4.1...v2.4.2)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Gemfile.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 0fe450d4ff..e95eac8cc0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -43,7 +43,7 @@ GEM
annotate (3.1.1)
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
- ast (2.4.1)
+ ast (2.4.2)
aws-eventstream (1.1.0)
aws-partitions (1.390.0)
aws-sdk-core (3.109.2)
From 0a1b1f1aa3b3ab60cc31d60657e2cd7dd5083a4e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 24 Jan 2021 22:46:23 +0100
Subject: [PATCH 34/85] Build(deps): Bump oj from 3.11.0 to 3.11.1 (#11824)
Bumps [oj](https://github.com/ohler55/oj) from 3.11.0 to 3.11.1.
- [Release notes](https://github.com/ohler55/oj/releases)
- [Changelog](https://github.com/ohler55/oj/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/ohler55/oj/compare/v3.11.0...v3.11.1)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Gemfile.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index e95eac8cc0..059eca05e7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -233,7 +233,7 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
- oj (3.11.0)
+ oj (3.11.1)
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
From 0a51999a4672d9d327d398ca49e3f4db59fccf9d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 24 Jan 2021 22:47:38 +0100
Subject: [PATCH 35/85] Build(deps): Bump rubocop-ast from 1.4.0 to 1.4.1
(#11823)
Bumps [rubocop-ast](https://github.com/rubocop-hq/rubocop-ast) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/rubocop-hq/rubocop-ast/releases)
- [Changelog](https://github.com/rubocop-hq/rubocop-ast/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop-hq/rubocop-ast/compare/v1.4.0...v1.4.1)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Gemfile.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 059eca05e7..898a0c2bb7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -372,7 +372,7 @@ GEM
rubocop-ast (>= 1.2.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
- rubocop-ast (1.4.0)
+ rubocop-ast (1.4.1)
parser (>= 2.7.1.5)
rubocop-discourse (2.4.1)
rubocop (>= 1.1.0)
From fcbb6c414338c4aa3cc6b0708984918541ee0389 Mon Sep 17 00:00:00 2001
From: Krzysztof Kotlarek
Date: Mon, 25 Jan 2021 09:35:13 +1100
Subject: [PATCH 36/85] FIX: remove rendering UX from bookmark model (#11765)
Fix for `bookmark.js` model. Most logic was moved to `topic` controller
---
.../app/components/bookmark-local-date.js | 57 ++++++++++
.../app/components/topic-list-item.js | 6 -
.../discourse/app/controllers/bookmark.js | 56 +++++-----
.../discourse/app/controllers/topic.js | 105 +++++++++++++++++-
.../javascripts/discourse/app/models/post.js | 69 +++---------
.../javascripts/discourse/app/models/topic.js | 70 +-----------
.../components/bookmark-local-date.hbs | 6 +
.../app/templates/modal/bookmark.hbs | 7 +-
.../initializers/new-user-narrative.js.es6 | 14 +--
9 files changed, 223 insertions(+), 167 deletions(-)
create mode 100644 app/assets/javascripts/discourse/app/components/bookmark-local-date.js
create mode 100644 app/assets/javascripts/discourse/app/templates/components/bookmark-local-date.hbs
diff --git a/app/assets/javascripts/discourse/app/components/bookmark-local-date.js b/app/assets/javascripts/discourse/app/components/bookmark-local-date.js
new file mode 100644
index 0000000000..d89c200073
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/bookmark-local-date.js
@@ -0,0 +1,57 @@
+import Component from "@ember/component";
+import I18n from "I18n";
+import { action } from "@ember/object";
+import { getOwner } from "discourse-common/lib/get-owner";
+import { or } from "@ember/object/computed";
+
+export default Component.extend({
+ tagName: "",
+
+ init() {
+ this._super(...arguments);
+
+ this.loadLocalDates();
+ },
+
+ get postLocalDateFormatted() {
+ return this.postLocalDate().format(I18n.t("dates.long_no_year"));
+ },
+
+ showPostLocalDate: or("postDetectedLocalDate", "postDetectedLocalTime"),
+
+ loadLocalDates() {
+ let postEl = document.querySelector(`[data-post-id="${this.postId}"]`);
+ let localDateEl = null;
+ if (postEl) {
+ localDateEl = postEl.querySelector(".discourse-local-date");
+ }
+
+ this.setProperties({
+ postDetectedLocalDate: localDateEl ? localDateEl.dataset.date : null,
+ postDetectedLocalTime: localDateEl ? localDateEl.dataset.time : null,
+ postDetectedLocalTimezone: localDateEl
+ ? localDateEl.dataset.timezone
+ : null,
+ });
+ },
+
+ postLocalDate() {
+ const bookmarkController = getOwner(this).lookup("controller:bookmark");
+ let parsedPostLocalDate = bookmarkController._parseCustomDateTime(
+ this.postDetectedLocalDate,
+ this.postDetectedLocalTime,
+ this.postDetectedLocalTimezone
+ );
+
+ if (!this.postDetectedLocalTime) {
+ return bookmarkController.startOfDay(parsedPostLocalDate);
+ }
+
+ return parsedPostLocalDate;
+ },
+
+ @action
+ setReminder() {
+ return this.onChange(this.postLocalDate());
+ },
+});
diff --git a/app/assets/javascripts/discourse/app/components/topic-list-item.js b/app/assets/javascripts/discourse/app/components/topic-list-item.js
index 22d67f2d00..2efef6a1f3 100644
--- a/app/assets/javascripts/discourse/app/components/topic-list-item.js
+++ b/app/assets/javascripts/discourse/app/components/topic-list-item.js
@@ -226,12 +226,6 @@ export default Component.extend({
return this.unhandledRowClick(e, topic);
},
- actions: {
- toggleBookmark() {
- this.topic.toggleBookmark().finally(() => this.renderTopicListItem());
- },
- },
-
unhandledRowClick() {},
navigateToTopic,
diff --git a/app/assets/javascripts/discourse/app/controllers/bookmark.js b/app/assets/javascripts/discourse/app/controllers/bookmark.js
index 5cab58bd0a..ec0e0010bb 100644
--- a/app/assets/javascripts/discourse/app/controllers/bookmark.js
+++ b/app/assets/javascripts/discourse/app/controllers/bookmark.js
@@ -1,5 +1,4 @@
import { REMINDER_TYPES, formattedReminderTime } from "discourse/lib/bookmark";
-import { and, or } from "@ember/object/computed";
import { isEmpty, isPresent } from "@ember/utils";
import { next, schedule } from "@ember/runloop";
import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark";
@@ -10,6 +9,7 @@ import ModalFunctionality from "discourse/mixins/modal-functionality";
import { Promise } from "rsvp";
import { action } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
+import { and } from "@ember/object/computed";
import bootbox from "bootbox";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
@@ -62,9 +62,7 @@ export default Controller.extend(ModalFunctionality, {
customReminderTime: null,
lastCustomReminderDate: null,
lastCustomReminderTime: null,
- postDetectedLocalDate: null,
- postDetectedLocalTime: null,
- postDetectedLocalTimezone: null,
+ postLocalDate: null,
mouseTrap: null,
userTimezone: null,
showOptions: false,
@@ -95,6 +93,8 @@ export default Controller.extend(ModalFunctionality, {
this._initializeExistingBookmarkData();
}
+ this.loadLocalDates();
+
schedule("afterRender", () => {
if (this.site.isMobileDevice) {
document.getElementById("bookmark-name").blur();
@@ -240,11 +240,6 @@ export default Controller.extend(ModalFunctionality, {
showLastCustom: and("lastCustomReminderTime", "lastCustomReminderDate"),
- showPostLocalDate: or(
- "model.postDetectedLocalDate",
- "model.postDetectedLocalTime"
- ),
-
get showLaterToday() {
let later = this.laterToday();
return (
@@ -302,8 +297,22 @@ export default Controller.extend(ModalFunctionality, {
return this.nextMonth().format(I18n.t("dates.long_no_year"));
},
- get postLocalDateFormatted() {
- return this.postLocalDate().format(I18n.t("dates.long_no_year"));
+ loadLocalDates() {
+ let postEl = document.querySelector(
+ `[data-post-id="${this.model.postId}"]`
+ );
+ let localDateEl = null;
+ if (postEl) {
+ localDateEl = postEl.querySelector(".discourse-local-date");
+ }
+
+ if (localDateEl) {
+ this.setProperties({
+ postDetectedLocalDate: localDateEl.dataset.date,
+ postDetectedLocalTime: localDateEl.dataset.time,
+ postDetectedLocalTimezone: localDateEl.dataset.timezone,
+ });
+ }
},
@discourseComputed("userTimezone")
@@ -442,7 +451,7 @@ export default Controller.extend(ModalFunctionality, {
case REMINDER_TYPES.LAST_CUSTOM:
return this.parsedLastCustomReminderDatetime;
case REMINDER_TYPES.POST_LOCAL_DATE:
- return this.postLocalDate();
+ return this.postLocalDate;
}
},
@@ -454,20 +463,6 @@ export default Controller.extend(ModalFunctionality, {
return this.startOfDay(this.now().add(1, "month"));
},
- postLocalDate() {
- let parsedPostLocalDate = this._parseCustomDateTime(
- this.model.postDetectedLocalDate,
- this.model.postDetectedLocalTime,
- this.model.postDetectedLocalTimezone
- );
-
- if (!this.model.postDetectedLocalTime) {
- return this.startOfDay(parsedPostLocalDate);
- }
-
- return parsedPostLocalDate;
- },
-
tomorrow() {
return this.startOfDay(this.now().add(1, "day"));
},
@@ -572,4 +567,13 @@ export default Controller.extend(ModalFunctionality, {
return this.saveAndClose();
}
},
+
+ @action
+ selectPostLocalDate(date) {
+ this.setProperties({
+ selectedReminderType: this.reminderTypes.POST_LOCAL_DATE,
+ postLocalDate: date,
+ });
+ return this.saveAndClose();
+ },
});
diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js
index e050aca6ea..7240ee39e9 100644
--- a/app/assets/javascripts/discourse/app/controllers/topic.js
+++ b/app/assets/javascripts/discourse/app/controllers/topic.js
@@ -702,9 +702,9 @@ export default Controller.extend(bufferedProperty("model"), {
if (!this.currentUser) {
return bootbox.alert(I18n.t("bookmarks.not_bookmarked"));
} else if (post) {
- return post.toggleBookmark();
+ return this._togglePostBookmark(post);
} else {
- return this.model.toggleBookmark().then((changedIds) => {
+ return this._toggleTopicBookmark(this.model).then((changedIds) => {
if (!changedIds) {
return;
}
@@ -1167,6 +1167,107 @@ export default Controller.extend(bufferedProperty("model"), {
}
},
+ _togglePostBookmark(post) {
+ return new Promise((resolve) => {
+ let modalController = showModal("bookmark", {
+ model: {
+ postId: post.id,
+ id: post.bookmark_id,
+ reminderAt: post.bookmark_reminder_at,
+ autoDeletePreference: post.bookmark_auto_delete_preference,
+ name: post.bookmark_name,
+ },
+ title: post.bookmark_id
+ ? "post.bookmarks.edit"
+ : "post.bookmarks.create",
+ modalClass: "bookmark-with-reminder",
+ });
+ modalController.setProperties({
+ onCloseWithoutSaving: () => {
+ resolve({ closedWithoutSaving: true });
+ post.appEvents.trigger("post-stream:refresh", { id: post.id });
+ },
+ afterSave: (savedData) => {
+ post.createBookmark(savedData);
+ resolve({ closedWithoutSaving: false });
+ },
+ afterDelete: (topicBookmarked) => {
+ post.deleteBookmark(topicBookmarked);
+ },
+ });
+ });
+ },
+
+ _toggleTopicBookmark() {
+ if (this.model.bookmarking) {
+ return Promise.resolve();
+ }
+ this.model.set("bookmarking", true);
+ const bookmark = !this.model.bookmarked;
+ let posts = this.model.postStream.posts;
+
+ return this.model.firstPost().then((firstPost) => {
+ const toggleBookmarkOnServer = () => {
+ if (bookmark) {
+ return this._togglePostBookmark(firstPost).then((opts) => {
+ this.model.set("bookmarking", false);
+ if (opts && opts.closedWithoutSaving) {
+ return;
+ }
+ return this.model.afterTopicBookmarked(firstPost);
+ });
+ } else {
+ return this.model
+ .deleteBookmark()
+ .then(() => {
+ this.model.toggleProperty("bookmarked");
+ this.model.set("bookmark_reminder_at", null);
+ let clearedBookmarkProps = {
+ bookmarked: false,
+ bookmark_id: null,
+ bookmark_name: null,
+ bookmark_reminder_at: null,
+ };
+ if (posts) {
+ const updated = [];
+ posts.forEach((post) => {
+ if (post.bookmarked) {
+ post.setProperties(clearedBookmarkProps);
+ updated.push(post.id);
+ }
+ });
+ firstPost.setProperties(clearedBookmarkProps);
+ return updated;
+ }
+ })
+ .catch(popupAjaxError)
+ .finally(() => this.model.set("bookmarking", false));
+ }
+ };
+
+ const unbookmarkedPosts = [];
+ if (!bookmark && posts) {
+ posts.forEach(
+ (post) => post.bookmarked && unbookmarkedPosts.push(post)
+ );
+ }
+
+ return new Promise((resolve) => {
+ if (unbookmarkedPosts.length > 1) {
+ bootbox.confirm(
+ I18n.t("bookmarks.confirm_clear"),
+ I18n.t("no_value"),
+ I18n.t("yes_value"),
+ (confirmed) =>
+ confirmed ? toggleBookmarkOnServer().then(resolve) : resolve()
+ );
+ } else {
+ toggleBookmarkOnServer().then(resolve);
+ }
+ });
+ });
+ },
+
togglePinnedState() {
this.send("togglePinnedForUser");
},
diff --git a/app/assets/javascripts/discourse/app/models/post.js b/app/assets/javascripts/discourse/app/models/post.js
index 5a8acd7c96..4c3a671ddf 100644
--- a/app/assets/javascripts/discourse/app/models/post.js
+++ b/app/assets/javascripts/discourse/app/models/post.js
@@ -16,7 +16,6 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
import { postUrl } from "discourse/lib/utilities";
import { propertyEqual } from "discourse/lib/computed";
import { resolveShareUrl } from "discourse/helpers/share-url";
-import showModal from "discourse/lib/show-modal";
import { userPath } from "discourse/lib/url";
const Post = RestModel.extend({
@@ -304,58 +303,24 @@ const Post = RestModel.extend({
return ajax(`/posts/${this.id}/unhide`, { type: "PUT" });
},
- toggleBookmark() {
- let postEl = document.querySelector(`[data-post-id="${this.id}"]`);
- let localDateEl = null;
- if (postEl) {
- localDateEl = postEl.querySelector(".discourse-local-date");
- }
-
- return new Promise((resolve) => {
- let controller = showModal("bookmark", {
- model: {
- postId: this.id,
- id: this.bookmark_id,
- reminderAt: this.bookmark_reminder_at,
- autoDeletePreference: this.bookmark_auto_delete_preference,
- name: this.bookmark_name,
- postDetectedLocalDate: localDateEl ? localDateEl.dataset.date : null,
- postDetectedLocalTime: localDateEl ? localDateEl.dataset.time : null,
- postDetectedLocalTimezone: localDateEl
- ? localDateEl.dataset.timezone
- : null,
- },
- title: this.bookmark_id
- ? "post.bookmarks.edit"
- : "post.bookmarks.create",
- modalClass: "bookmark-with-reminder",
- });
- controller.setProperties({
- onCloseWithoutSaving: () => {
- resolve({ closedWithoutSaving: true });
- this.appEvents.trigger("post-stream:refresh", { id: this.id });
- },
- afterSave: (savedData) => {
- this.setProperties({
- "topic.bookmarked": true,
- bookmarked: true,
- bookmark_reminder_at: savedData.reminderAt,
- bookmark_reminder_type: savedData.reminderType,
- bookmark_auto_delete_preference: savedData.autoDeletePreference,
- bookmark_name: savedData.name,
- bookmark_id: savedData.id,
- });
- resolve({ closedWithoutSaving: false });
- this.appEvents.trigger("page:bookmark-post-toggled", this);
- this.appEvents.trigger("post-stream:refresh", { id: this.id });
- },
- afterDelete: (topicBookmarked) => {
- this.set("topic.bookmarked", topicBookmarked);
- this.clearBookmark();
- this.appEvents.trigger("page:bookmark-post-toggled", this);
- },
- });
+ createBookmark(data) {
+ this.setProperties({
+ "topic.bookmarked": true,
+ bookmarked: true,
+ bookmark_reminder_at: data.reminderAt,
+ bookmark_reminder_type: data.reminderType,
+ bookmark_auto_delete_preference: data.autoDeletePreference,
+ bookmark_name: data.name,
+ bookmark_id: data.id,
});
+ this.appEvents.trigger("page:bookmark-post-toggled", this);
+ this.appEvents.trigger("post-stream:refresh", { id: this.id });
+ },
+
+ deleteBookmark(bookmarked) {
+ this.set("topic.bookmarked", bookmarked);
+ this.clearBookmark();
+ this.appEvents.trigger("page:bookmark-post-toggled", this);
},
clearBookmark() {
diff --git a/app/assets/javascripts/discourse/app/models/topic.js b/app/assets/javascripts/discourse/app/models/topic.js
index 8796bc4261..6de4b88f73 100644
--- a/app/assets/javascripts/discourse/app/models/topic.js
+++ b/app/assets/javascripts/discourse/app/models/topic.js
@@ -11,7 +11,6 @@ import Session from "discourse/models/session";
import Site from "discourse/models/site";
import User from "discourse/models/user";
import { ajax } from "discourse/lib/ajax";
-import bootbox from "bootbox";
import { deepMerge } from "discourse-common/lib/object";
import discourseComputed from "discourse-common/utils/decorators";
import { emojiUnescape } from "discourse/lib/text";
@@ -404,73 +403,8 @@ const Topic = RestModel.extend({
}
},
- toggleBookmark() {
- if (this.bookmarking) {
- return Promise.resolve();
- }
- this.set("bookmarking", true);
- const bookmark = !this.bookmarked;
- let posts = this.postStream.posts;
-
- return this.firstPost().then((firstPost) => {
- const toggleBookmarkOnServer = () => {
- if (bookmark) {
- return firstPost.toggleBookmark().then((opts) => {
- this.set("bookmarking", false);
- if (opts.closedWithoutSaving) {
- return;
- }
- return this.afterTopicBookmarked(firstPost);
- });
- } else {
- return ajax(`/t/${this.id}/remove_bookmarks`, { type: "PUT" })
- .then(() => {
- this.toggleProperty("bookmarked");
- this.set("bookmark_reminder_at", null);
- let clearedBookmarkProps = {
- bookmarked: false,
- bookmark_id: null,
- bookmark_name: null,
- bookmark_reminder_at: null,
- };
- if (posts) {
- const updated = [];
- posts.forEach((post) => {
- if (post.bookmarked) {
- post.setProperties(clearedBookmarkProps);
- updated.push(post.id);
- }
- });
- firstPost.setProperties(clearedBookmarkProps);
- return updated;
- }
- })
- .catch(popupAjaxError)
- .finally(() => this.set("bookmarking", false));
- }
- };
-
- const unbookmarkedPosts = [];
- if (!bookmark && posts) {
- posts.forEach(
- (post) => post.bookmarked && unbookmarkedPosts.push(post)
- );
- }
-
- return new Promise((resolve) => {
- if (unbookmarkedPosts.length > 1) {
- bootbox.confirm(
- I18n.t("bookmarks.confirm_clear"),
- I18n.t("no_value"),
- I18n.t("yes_value"),
- (confirmed) =>
- confirmed ? toggleBookmarkOnServer().then(resolve) : resolve()
- );
- } else {
- toggleBookmarkOnServer().then(resolve);
- }
- });
- });
+ deleteBookmark() {
+ return ajax(`/t/${this.id}/remove_bookmarks`, { type: "PUT" });
},
createGroupInvite(group) {
diff --git a/app/assets/javascripts/discourse/app/templates/components/bookmark-local-date.hbs b/app/assets/javascripts/discourse/app/templates/components/bookmark-local-date.hbs
new file mode 100644
index 0000000000..a30363826d
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/templates/components/bookmark-local-date.hbs
@@ -0,0 +1,6 @@
+{{#if showPostLocalDate}}
+ {{#tap-tile icon="globe-americas" tileId=tileId activeTile=activeTile onChange=(action "setReminder")}}
+ {{i18n "bookmarks.reminders.post_local_date"}}
+ {{postLocalDateFormatted}}
+ {{/tap-tile}}
+{{/if}}
diff --git a/app/assets/javascripts/discourse/app/templates/modal/bookmark.hbs b/app/assets/javascripts/discourse/app/templates/modal/bookmark.hbs
index ba19e49d42..46049bb9b7 100644
--- a/app/assets/javascripts/discourse/app/templates/modal/bookmark.hbs
+++ b/app/assets/javascripts/discourse/app/templates/modal/bookmark.hbs
@@ -65,12 +65,7 @@
{{i18n "bookmarks.reminders.next_month"}}
{{nextMonthFormatted}}
{{/tap-tile}}
- {{#if showPostLocalDate}}
- {{#tap-tile icon="globe-americas" tileId=reminderTypes.POST_LOCAL_DATE activeTile=grid.activeTile onChange=(action "selectReminderType")}}
- {{i18n "bookmarks.reminders.post_local_date"}}
- {{postLocalDateFormatted}}
- {{/tap-tile}}
- {{/if}}
+ {{bookmark-local-date postId=model.postId tileId=reminderTypes.POST_LOCAL_DATE activeTile=grid.activeTile onChange=(action "selectPostLocalDate")}}
{{#tap-tile icon="calendar-alt" tileId=reminderTypes.CUSTOM activeTile=grid.activeTile onChange=(action "selectReminderType")}}
{{i18n "bookmarks.reminders.custom"}}
{{/tap-tile}}
diff --git a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6 b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6
index 3cac3b63e0..156c4dd0cd 100644
--- a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6
+++ b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6
@@ -13,26 +13,26 @@ function initialize(api) {
},
});
- api.modifyClass("model:post", {
- toggleBookmark() {
+ api.modifyClass("controller:topic", {
+ _togglePostBookmark(post) {
// if we are talking to discobot then any bookmarks should just
// be created without reminder options, to streamline the new user
// narrative.
const discobotUserId = -2;
- if (this.user_id === discobotUserId && !this.bookmarked) {
+ if (post.user_id === discobotUserId && !post.bookmarked) {
return ajax("/bookmarks", {
type: "POST",
- data: { post_id: this.id },
+ data: { post_id: post.id },
}).then((response) => {
- this.setProperties({
+ post.setProperties({
"topic.bookmarked": true,
bookmarked: true,
bookmark_id: response.id,
});
- this.appEvents.trigger("post-stream:refresh", { id: this.id });
+ post.appEvents.trigger("post-stream:refresh", { id: this.id });
});
}
- return this._super();
+ return this._super(post);
},
});
From aa1138ff718925a064c987bc8b3dde1bfcbba210 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?=
Date: Mon, 25 Jan 2021 11:23:36 +0100
Subject: [PATCH 37/85] FIX: reindex_search job should work on model with no
search data (#11819)
Lots of changes but it's mostly a refactoring.
The interesting part that was fix are the 'load_problem__ids' methods.
They will now return records with no search data associated so they can be properly indexed for the search.
This "bad" state usually happens after a migration.
---
app/controllers/users_controller.rb | 27 +-
app/jobs/scheduled/reindex_search.rb | 261 ++++++++----------
app/models/user_search.rb | 153 +++++-----
spec/components/search_spec.rb | 4 +-
spec/jobs/reindex_search_spec.rb | 25 +-
.../similar_topics_controller_spec.rb | 2 +-
6 files changed, 204 insertions(+), 268 deletions(-)
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 2c4163e986..651fda8340 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1041,18 +1041,14 @@ class UsersController < ApplicationController
def search_users
term = params[:term].to_s.strip
- topic_id = params[:topic_id]
- topic_id = topic_id.to_i if topic_id
-
- category_id = params[:category_id].to_i if category_id.present?
+ topic_id = params[:topic_id].to_i if params[:topic_id].present?
+ category_id = params[:category_id].to_i if params[:category_id].present?
topic_allowed_users = params[:topic_allowed_users] || false
group_names = params[:groups] || []
group_names << params[:group] if params[:group]
- if group_names.present?
- @groups = Group.where(name: group_names)
- end
+ @groups = Group.where(name: group_names) if group_names.present?
options = {
topic_allowed_users: topic_allowed_users,
@@ -1060,13 +1056,8 @@ class UsersController < ApplicationController
groups: @groups
}
- if topic_id
- options[:topic_id] = topic_id
- end
-
- if category_id
- options[:category_id] = category_id
- end
+ options[:topic_id] = topic_id if topic_id
+ options[:category_id] = category_id if category_id
results = UserSearch.new(term, options).search
@@ -1075,8 +1066,10 @@ class UsersController < ApplicationController
to_render = { users: results.as_json(only: user_fields, methods: [:avatar_template]) }
+ # blank term is only handy for in-topic search of users after @
+ # we do not want group results ever if term is blank
groups =
- if current_user
+ if term.present? && current_user
if params[:include_groups] == 'true'
Group.visible_groups(current_user)
elsif params[:include_mentionable_groups] == 'true'
@@ -1086,10 +1079,6 @@ class UsersController < ApplicationController
end
end
- # blank term is only handy for in-topic search of users after @
- # we do not want group results ever if term is blank
- groups = nil if term.blank?
-
if groups
groups = Group.search_groups(term, groups: groups)
groups = groups.order('groups.name asc')
diff --git a/app/jobs/scheduled/reindex_search.rb b/app/jobs/scheduled/reindex_search.rb
index 3d6579c78f..26215cdc7a 100644
--- a/app/jobs/scheduled/reindex_search.rb
+++ b/app/jobs/scheduled/reindex_search.rb
@@ -1,201 +1,162 @@
# frozen_string_literal: true
module Jobs
- # if locale changes or search algorithm changes we may want to reindex stuff
class ReindexSearch < ::Jobs::Scheduled
every 2.hours
- CLEANUP_GRACE_PERIOD = 1.day.ago
-
def execute(args)
- @verbose = true if args && Hash === args && args[:verbose]
+ @verbose = args[:verbose]
+ @cleanup_grace_period = 1.day.ago
- rebuild_problem_topics
- rebuild_problem_posts
- rebuild_problem_categories
- rebuild_problem_users
- rebuild_problem_tags
- clean_post_search_data
- clean_topic_search_data
+ rebuild_categories
+ rebuild_tags
+ rebuild_topics
+ rebuild_posts
+ rebuild_users
- @verbose = nil
+ clean_topics
+ clean_posts
end
- def rebuild_problem_categories(limit: 500)
+ def rebuild_categories(limit: 500, indexer: SearchIndexer)
category_ids = load_problem_category_ids(limit)
- if @verbose
- puts "rebuilding #{category_ids.length} categories"
- end
+ puts "rebuilding #{category_ids.size} categories" if @verbose
category_ids.each do |id|
category = Category.find_by(id: id)
- SearchIndexer.index(category, force: true) if category
+ indexer.index(category, force: true) if category
end
end
- 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
- end
- end
-
- 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, 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
-
- def rebuild_problem_tags(limit: 10000)
+ def rebuild_tags(limit: 1_000, indexer: SearchIndexer)
tag_ids = load_problem_tag_ids(limit)
- if @verbose
- puts "rebuilding #{tag_ids.length} tags"
- end
+ puts "rebuilding #{tag_ids.size} tags" if @verbose
tag_ids.each do |id|
tag = Tag.find_by(id: id)
- SearchIndexer.index(tag, force: true) if tag
+ indexer.index(tag, force: true) if tag
end
end
- private
+ def rebuild_topics(limit: 10_000, indexer: SearchIndexer)
+ topic_ids = load_problem_topic_ids(limit)
- def clean_post_search_data
- puts "cleaning up post search data" if @verbose
+ puts "rebuilding #{topic_ids.size} topics" if @verbose
- PostSearchData
- .joins("LEFT JOIN posts p ON p.id = post_search_data.post_id")
- .where("p.raw = ''")
- .delete_all
-
- DB.exec(<<~SQL, deleted_at: CLEANUP_GRACE_PERIOD)
- DELETE FROM post_search_data
- WHERE post_id IN (
- SELECT post_id
- FROM post_search_data
- LEFT JOIN posts ON post_search_data.post_id = posts.id
- INNER JOIN topics ON posts.topic_id = topics.id
- WHERE (topics.deleted_at IS NOT NULL
- AND topics.deleted_at <= :deleted_at) OR (
- posts.deleted_at IS NOT NULL AND
- posts.deleted_at <= :deleted_at
- )
-
- )
- SQL
+ topic_ids.each do |id|
+ topic = Topic.find_by(id: id)
+ indexer.index(topic, force: true) if topic
+ end
end
- def clean_topic_search_data
+ def rebuild_posts(limit: 20_000, indexer: SearchIndexer)
+ post_ids = load_problem_post_ids(limit)
+
+ puts "rebuilding #{post_ids.size} posts" if @verbose
+
+ post_ids.each do |id|
+ post = Post.find_by(id: id)
+ indexer.index(post, force: true) if post
+ end
+ end
+
+ def rebuild_users(limit: 5_000, indexer: SearchIndexer)
+ user_ids = load_problem_user_ids(limit)
+
+ puts "rebuilding #{user_ids.size} users" if @verbose
+
+ user_ids.each do |id|
+ user = User.find_by(id: id)
+ indexer.index(user, force: true) if user
+ end
+ end
+
+ def clean_topics
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 (
- SELECT topic_id
- FROM topic_search_data
- INNER JOIN topics ON topic_search_data.topic_id = topics.id
- WHERE topics.deleted_at IS NOT NULL
- AND topics.deleted_at <= :deleted_at
- )
+ # remove search data from deleted topics
+
+ DB.exec(<<~SQL, deleted_at: @cleanup_grace_period)
+ DELETE FROM topic_search_data
+ WHERE topic_id IN (
+ SELECT topic_id
+ FROM topic_search_data
+ LEFT JOIN topics ON topic_id = topics.id
+ WHERE topics.id IS NULL
+ OR (deleted_at IS NOT NULL AND deleted_at <= :deleted_at)
+ )
SQL
end
- def load_problem_post_ids(limit)
- params = {
- locale: SiteSetting.default_locale,
- version: SearchIndexer::MIN_POST_REINDEX_VERSION,
- limit: limit
- }
+ def clean_posts
+ puts "cleaning up post search data" if @verbose
- DB.query_single(<<~SQL, params)
- SELECT
- posts.id
- FROM posts
- JOIN topics ON topics.id = posts.topic_id
- LEFT JOIN post_search_data pd
- ON pd.locale = :locale
- AND pd.version >= :version
- AND pd.post_id = posts.id
- WHERE pd.post_id IS NULL
- AND posts.deleted_at IS NULL
- AND topics.deleted_at IS NULL
- AND posts.raw != ''
- ORDER BY posts.id DESC
- LIMIT :limit
+ # remove search data from deleted/empty posts
+
+ DB.exec(<<~SQL, deleted_at: @cleanup_grace_period)
+ DELETE FROM post_search_data
+ WHERE post_id IN (
+ SELECT post_id
+ FROM post_search_data
+ LEFT JOIN posts ON post_id = posts.id
+ JOIN topics ON posts.topic_id = topics.id
+ WHERE posts.id IS NULL
+ OR posts.raw = ''
+ OR (posts.deleted_at IS NOT NULL AND posts.deleted_at <= :deleted_at)
+ OR (topics.deleted_at IS NOT NULL AND topics.deleted_at <= :deleted_at)
+ )
SQL
end
def load_problem_category_ids(limit)
- Category.joins(:category_search_data)
- .where('category_search_data.locale != ?
- OR category_search_data.version != ?', SiteSetting.default_locale, SearchIndexer::CATEGORY_INDEX_VERSION)
- .order('categories.id asc')
- .limit(limit)
- .pluck(:id)
- end
-
- def load_problem_topic_ids(limit)
- Topic.joins(:topic_search_data)
- .where('topic_search_data.locale != ?
- OR topic_search_data.version != ?', SiteSetting.default_locale, SearchIndexer::TOPIC_INDEX_VERSION)
- .order('topics.id desc')
- .limit(limit)
- .pluck(:id)
- end
-
- def load_problem_user_ids(limit)
- User.joins(:user_search_data)
- .where('user_search_data.locale != ?
- OR user_search_data.version != ?', SiteSetting.default_locale, SearchIndexer::USER_INDEX_VERSION)
- .order('users.id asc')
+ Category
+ .joins("LEFT JOIN category_search_data ON category_id = categories.id")
+ .where("category_search_data.locale IS NULL OR category_search_data.locale != ? OR category_search_data.version != ?", SiteSetting.default_locale, SearchIndexer::CATEGORY_INDEX_VERSION)
+ .order("categories.id ASC")
.limit(limit)
.pluck(:id)
end
def load_problem_tag_ids(limit)
- Tag.joins(:tag_search_data)
- .where('tag_search_data.locale != ?
- OR tag_search_data.version != ?', SiteSetting.default_locale, SearchIndexer::TAG_INDEX_VERSION)
- .order('tags.id asc')
+ Tag
+ .joins("LEFT JOIN tag_search_data ON tag_id = tags.id")
+ .where("tag_search_data.locale IS NULL OR tag_search_data.locale != ? OR tag_search_data.version != ?", SiteSetting.default_locale, SearchIndexer::TAG_INDEX_VERSION)
+ .order("tags.id ASC")
.limit(limit)
.pluck(:id)
end
+
+ def load_problem_topic_ids(limit)
+ Topic
+ .joins("LEFT JOIN topic_search_data ON topic_id = topics.id")
+ .where("topic_search_data.locale IS NULL OR topic_search_data.locale != ? OR topic_search_data.version != ?", SiteSetting.default_locale, SearchIndexer::TOPIC_INDEX_VERSION)
+ .order("topics.id DESC")
+ .limit(limit)
+ .pluck(:id)
+ end
+
+ def load_problem_post_ids(limit)
+ Post
+ .joins(:topic)
+ .joins("LEFT JOIN post_search_data ON post_id = posts.id")
+ .where("posts.raw != ''")
+ .where("topics.deleted_at IS NULL")
+ .where("post_search_data.locale IS NULL OR post_search_data.locale != ? OR post_search_data.version != ?", SiteSetting.default_locale, SearchIndexer::POST_INDEX_VERSION)
+ .order("posts.id DESC")
+ .limit(limit)
+ .pluck(:id)
+ end
+
+ def load_problem_user_ids(limit)
+ User
+ .joins("LEFT JOIN user_search_data ON user_id = users.id")
+ .where("user_search_data.locale IS NULL OR user_search_data.locale != ? OR user_search_data.version != ?", SiteSetting.default_locale, SearchIndexer::USER_INDEX_VERSION)
+ .order("users.id ASC")
+ .limit(limit)
+ .pluck(:id)
+ end
+
end
end
diff --git a/app/models/user_search.rb b/app/models/user_search.rb
index 447f5d4492..2b9b05d0f8 100644
--- a/app/models/user_search.rb
+++ b/app/models/user_search.rb
@@ -1,13 +1,12 @@
# frozen_string_literal: true
-# Searches for a user by username or full text or name (if enabled in SiteSettings)
class UserSearch
MAX_SIZE_PRIORITY_MENTION ||= 500
def initialize(term, opts = {})
- @term = term
- @term_like = "#{term.downcase.gsub("_", "\\_")}%"
+ @term = term.downcase
+ @term_like = @term.gsub("_", "\\_") + "%"
@topic_id = opts[:topic_id]
@category_id = opts[:category_id]
@topic_allowed_users = opts[:topic_allowed_users]
@@ -15,131 +14,125 @@ class UserSearch
@include_staged_users = opts[:include_staged_users] || false
@limit = opts[:limit] || 20
@groups = opts[:groups]
+
+ @topic = Topic.find(@topic_id) if @topic_id
+ @category = Category.find(@category_id) if @category_id
+
@guardian = Guardian.new(@searching_user)
- @guardian.ensure_can_see_groups_members! @groups if @groups
- @guardian.ensure_can_see_category! Category.find(@category_id) if @category_id
- @guardian.ensure_can_see_topic! Topic.find(@topic_id) if @topic_id
+ @guardian.ensure_can_see_groups_members!(@groups) if @groups
+ @guardian.ensure_can_see_category!(@category) if @category
+ @guardian.ensure_can_see_topic!(@topic) if @topic
end
def scoped_users
users = User.where(active: true)
users = users.where(staged: false) unless @include_staged_users
+ users = users.not_suspended unless @searching_user&.staff?
if @groups
- users = users.joins("INNER JOIN group_users ON group_users.user_id = users.id")
+ users = users
+ .joins(:group_users)
.where("group_users.group_id IN (?)", @groups.map(&:id))
end
- unless @searching_user && @searching_user.staff?
- users = users.not_suspended
- end
-
# Only show users who have access to private topic
- if @topic_id && @topic_allowed_users == "true"
- topic = Topic.find_by(id: @topic_id)
-
- if topic.category && topic.category.read_restricted
- users = users.includes(:secure_categories)
- .where("users.admin = TRUE OR categories.id = ?", topic.category.id)
- .references(:categories)
- end
- end
-
- users.limit(@limit)
- end
-
- def filtered_by_term_users
- users = scoped_users
-
- if @term.present?
- if SiteSetting.enable_names? && @term !~ /[_\.-]/
- query = Search.ts_query(term: @term, ts_config: "simple")
-
- users = users.includes(:user_search_data)
- .references(:user_search_data)
- .where("user_search_data.search_data @@ #{query}")
- .order(DB.sql_fragment("CASE WHEN username_lower LIKE ? THEN 0 ELSE 1 END ASC", @term_like))
-
- else
- users = users.where("username_lower LIKE :term_like", term_like: @term_like)
- end
+ if @topic_allowed_users == "true" && @topic&.category&.read_restricted
+ users = users
+ .references(:categories)
+ .includes(:secure_categories)
+ .where("users.admin OR categories.id = ?", @topic.category_id)
end
users
end
+ def filtered_by_term_users
+ if @term.blank?
+ scoped_users
+ elsif SiteSetting.enable_names? && @term !~ /[_\.-]/
+ query = Search.ts_query(term: @term, ts_config: "simple")
+
+ scoped_users
+ .includes(:user_search_data)
+ .where("user_search_data.search_data @@ #{query}")
+ .order(DB.sql_fragment("CASE WHEN username_lower LIKE ? THEN 0 ELSE 1 END ASC", @term_like))
+ else
+ scoped_users.where("username_lower LIKE :term_like", term_like: @term_like)
+ end
+ end
+
def search_ids
users = Set.new
# 1. exact username matches
if @term.present?
- scoped_users.where(username_lower: @term.downcase)
+ scoped_users
+ .where(username_lower: @term)
.limit(@limit)
.pluck(:id)
.each { |id| users << id }
-
end
- return users.to_a if users.length >= @limit
+ return users.to_a if users.size >= @limit
# 2. in topic
if @topic_id
in_topic = filtered_by_term_users
- .where('users.id IN (SELECT p.user_id FROM posts p WHERE topic_id = ?)', @topic_id)
+ .where('users.id IN (SELECT user_id FROM posts WHERE topic_id = ?)', @topic_id)
if @searching_user.present?
in_topic = in_topic.where('users.id <> ?', @searching_user.id)
end
in_topic
- .order('last_seen_at DESC')
- .limit(@limit - users.length)
+ .order('last_seen_at DESC NULLS LAST')
+ .limit(@limit - users.size)
.pluck(:id)
.each { |id| users << id }
end
- return users.to_a if users.length >= @limit
+ return users.to_a if users.size >= @limit
- secure_category_id = nil
+ # 3. in category
+ secure_category_id =
+ if @category_id
+ DB.query_single(<<~SQL, @category_id).first
+ SELECT id
+ FROM categories
+ WHERE read_restricted
+ AND id = ?
+ SQL
+ elsif @topic_id
+ DB.query_single(<<~SQL, @topic_id).first
+ SELECT id
+ FROM categories
+ WHERE read_restricted
+ AND id IN (SELECT category_id FROM topics WHERE id = ?)
+ SQL
+ end
- if @category_id
- secure_category_id = DB.query_single(<<~SQL, @category_id).first
- SELECT id FROM categories
- WHERE read_restricted AND id = ?
- SQL
- elsif @topic_id
- secure_category_id = DB.query_single(<<~SQL, @topic_id).first
- SELECT id FROM categories
- WHERE read_restricted AND id IN (
- SELECT category_id FROM topics
- WHERE id = ?
- )
- SQL
- end
-
- # 3. category matches
if secure_category_id
- @searching_user.present?
-
category_groups = Group.where(<<~SQL, secure_category_id, MAX_SIZE_PRIORITY_MENTION)
groups.id IN (
- SELECT group_id FROM category_groups
- JOIN groups g ON group_id = g.id
- WHERE
- category_id = ? AND
- user_count < ?
+ SELECT group_id
+ FROM category_groups
+ JOIN groups g ON group_id = g.id
+ WHERE category_id = ?
+ AND user_count < ?
)
SQL
- category_groups = category_groups.members_visible_groups(@searching_user)
+ if @searching_user.present?
+ category_groups = category_groups.members_visible_groups(@searching_user)
+ end
in_category = filtered_by_term_users
.where(<<~SQL, category_groups.pluck(:id))
users.id IN (
SELECT gu.user_id
- FROM group_users gu
- WHERE group_id IN (?)
- LIMIT 200
+ FROM group_users gu
+ WHERE group_id IN (?)
+ LIMIT 200
)
SQL
@@ -148,17 +141,19 @@ class UserSearch
end
in_category
- .order('last_seen_at DESC')
- .limit(@limit - users.length)
+ .order('last_seen_at DESC NULLS LAST')
+ .limit(@limit - users.size)
.pluck(:id)
.each { |id| users << id }
end
- return users.to_a if users.length >= @limit
+ return users.to_a if users.size >= @limit
+
# 4. global matches
if @term.present?
- filtered_by_term_users.order('last_seen_at DESC')
- .limit(@limit - users.length)
+ filtered_by_term_users
+ .order('last_seen_at DESC NULLS LAST')
+ .limit(@limit - users.size)
.pluck(:id)
.each { |id| users << id }
end
diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb
index b34e2c4080..16a583a2a5 100644
--- a/spec/components/search_spec.rb
+++ b/spec/components/search_spec.rb
@@ -961,7 +961,7 @@ describe Search do
it 'can find posts with tags' do
# we got to make this index (it is deferred)
- Jobs::ReindexSearch.new.rebuild_problem_posts
+ Jobs::ReindexSearch.new.rebuild_posts
result = Search.execute(tag.name)
expect(result.posts.length).to eq(1)
@@ -977,7 +977,7 @@ describe Search do
it 'can find posts with tag synonyms' do
synonym = Fabricate(:tag, name: 'synonym', target_tag: tag)
- Jobs::ReindexSearch.new.rebuild_problem_posts
+ Jobs::ReindexSearch.new.rebuild_posts
result = Search.execute(synonym.name)
expect(result.posts.length).to eq(1)
end
diff --git a/spec/jobs/reindex_search_spec.rb b/spec/jobs/reindex_search_spec.rb
index b6132855e2..3128aeccb1 100644
--- a/spec/jobs/reindex_search_spec.rb
+++ b/spec/jobs/reindex_search_spec.rb
@@ -34,7 +34,7 @@ describe Jobs::ReindexSearch do
end
end
- describe 'rebuild_problem_posts' do
+ describe 'rebuild_posts' do
class FakeIndexer
def self.index(post, force:)
get_posts.push(post)
@@ -59,10 +59,7 @@ describe Jobs::ReindexSearch do
FakeIndexer.reset
end
- it (
- 'should not reindex posts that belong to a deleted topic ' \
- 'or have been trashed'
- ) do
+ it "should not reindex posts that belong to a deleted topic or have been trashed" do
post = Fabricate(:post)
post2 = Fabricate(:post)
post3 = Fabricate(:post)
@@ -70,7 +67,7 @@ describe Jobs::ReindexSearch do
post2.topic.trash!
post3.trash!
- subject.rebuild_problem_posts(indexer: FakeIndexer)
+ subject.rebuild_posts(indexer: FakeIndexer)
expect(FakeIndexer.posts).to contain_exactly(post)
end
@@ -78,7 +75,7 @@ describe Jobs::ReindexSearch do
it 'should not reindex posts with a developmental version' do
post = Fabricate(:post, version: SearchIndexer::MIN_POST_REINDEX_VERSION + 1)
- subject.rebuild_problem_posts(indexer: FakeIndexer)
+ subject.rebuild_posts(indexer: FakeIndexer)
expect(FakeIndexer.posts).to eq([])
end
@@ -94,7 +91,7 @@ describe Jobs::ReindexSearch do
post2.save!(validate: false)
- subject.rebuild_problem_posts(indexer: FakeIndexer)
+ subject.rebuild_posts(indexer: FakeIndexer)
expect(FakeIndexer.posts).to contain_exactly(post)
end
@@ -107,9 +104,7 @@ describe Jobs::ReindexSearch do
[topic, topic2].each { |t| SearchIndexer.index(t, force: true) }
- freeze_time(described_class::CLEANUP_GRACE_PERIOD) do
- topic.trash!
- end
+ freeze_time(1.day.ago) { topic.trash! }
expect { subject.execute({}) }.to change { TopicSearchData.count }.by(-1)
expect(Topic.pluck(:id)).to contain_exactly(topic2.id)
@@ -119,11 +114,7 @@ describe Jobs::ReindexSearch do
)
end
- it(
- "should clean up post_search_data of posts with empty raw or posts from " \
- "trashed topics"
- ) do
-
+ it "should clean up post_search_data of posts with empty raw or posts from trashed topics" do
post = Fabricate(:post)
post2 = Fabricate(:post, post_type: Post.types[:small_action])
post2.raw = ""
@@ -132,7 +123,7 @@ describe Jobs::ReindexSearch do
post3.topic.trash!
post4, post5, post6 = nil
- freeze_time(described_class::CLEANUP_GRACE_PERIOD) do
+ freeze_time(1.day.ago) do
post4 = Fabricate(:post)
post4.topic.trash!
diff --git a/spec/requests/similar_topics_controller_spec.rb b/spec/requests/similar_topics_controller_spec.rb
index cef7f5e205..8ecc048668 100644
--- a/spec/requests/similar_topics_controller_spec.rb
+++ b/spec/requests/similar_topics_controller_spec.rb
@@ -17,7 +17,7 @@ describe SimilarTopicsController do
def reindex_posts
SearchIndexer.enable
- Jobs::ReindexSearch.new.rebuild_problem_posts
+ Jobs::ReindexSearch.new.rebuild_posts
end
it "requires a title param" do
From 8edd2b38cb2959fce7d97626cd17e1d463871662 Mon Sep 17 00:00:00 2001
From: Joffrey JAFFEUX
Date: Mon, 25 Jan 2021 11:30:59 +0100
Subject: [PATCH 38/85] FIX: ensures timeline_lookup includes last tuple
(#11829)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A simplified version of the logic used in the function before my fix is as follow:
```ruby
result = []
things = [0,1,2,3]
max_values = 2
every = (things.size.to_f / max_values).ceil
things.each_with_index do |t, index|
next unless (t % every) === 0
result << t
end
p result # [0, 2]
# 3 doesn’t get included
```
The problem is that if you get unlucky two times you won't get last tuple(s) and might get a very erroneous date.
Double unlucky:
- last tuple index % computed every !== 0 and you don't get the last tuple
- the last tuple is related to a post with a very different date than the previous tuples (on year difference in our case)
---
lib/timeline_lookup.rb | 5 ++++-
spec/lib/timeline_lookup_spec.rb | 17 +++++++++++++++++
2 files changed, 21 insertions(+), 1 deletion(-)
create mode 100644 spec/lib/timeline_lookup_spec.rb
diff --git a/lib/timeline_lookup.rb b/lib/timeline_lookup.rb
index 2e6cf57c82..f477153f94 100644
--- a/lib/timeline_lookup.rb
+++ b/lib/timeline_lookup.rb
@@ -12,7 +12,10 @@ module TimelineLookup
last_days_ago = -1
tuples.each_with_index do |t, idx|
return result unless t.is_a?(Array)
- next unless (idx % every) === 0
+
+ if idx != tuples.size - 1
+ next unless (idx % every) === 0
+ end
days_ago = t[1]
diff --git a/spec/lib/timeline_lookup_spec.rb b/spec/lib/timeline_lookup_spec.rb
new file mode 100644
index 0000000000..517b038c57
--- /dev/null
+++ b/spec/lib/timeline_lookup_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe TimelineLookup do
+
+ context '.build' do
+ it 'keeps the last tuple in the lookup' do
+ tuples = [
+ [7173, 400], [7174, 390], [7175, 380], [7176, 370], [7177, 1]
+ ]
+
+ expect(TimelineLookup.build(tuples, 2)).to eq([[1, 400], [4, 370], [5, 1]])
+ end
+ end
+
+end
From bed011feef23f7cc12fde9762fe76849a7c04479 Mon Sep 17 00:00:00 2001
From: Joffrey JAFFEUX
Date: Mon, 25 Jan 2021 11:31:52 +0100
Subject: [PATCH 39/85] A11Y: uses role=button and supports ariaPressed for
tapTile (#11827)
---
.../discourse/app/components/tap-tile.js | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/app/assets/javascripts/discourse/app/components/tap-tile.js b/app/assets/javascripts/discourse/app/components/tap-tile.js
index b384ff7d71..8671cc1ad2 100644
--- a/app/assets/javascripts/discourse/app/components/tap-tile.js
+++ b/app/assets/javascripts/discourse/app/components/tap-tile.js
@@ -1,13 +1,26 @@
+import { reads } from "@ember/object/computed";
import Component from "@ember/component";
import { propertyEqual } from "discourse/lib/computed";
export default Component.extend({
init() {
this._super(...arguments);
+
this.set("elementId", `tap_tile_${this.tileId}`);
},
+
classNames: ["tap-tile"],
+
classNameBindings: ["active"],
+
+ attributeBindings: ["role", "ariaPressed", "tabIndex"],
+
+ role: "button",
+
+ tabIndex: 0,
+
+ ariaPressed: reads("active"),
+
click() {
this.onChange(this.tileId);
},
From 21d66032452d3593c25e9c09365dac8fbf8cc2aa Mon Sep 17 00:00:00 2001
From: Joffrey JAFFEUX
Date: Mon, 25 Jan 2021 12:04:27 +0100
Subject: [PATCH 40/85] DEV: followup to 8edd2b38cb to use existing spec
(#11830)
This commit also better explains in spec why max_values might be off by one.
---
spec/components/timeline_lookup_spec.rb | 11 +++++++----
spec/lib/timeline_lookup_spec.rb | 17 -----------------
2 files changed, 7 insertions(+), 21 deletions(-)
delete mode 100644 spec/lib/timeline_lookup_spec.rb
diff --git a/spec/components/timeline_lookup_spec.rb b/spec/components/timeline_lookup_spec.rb
index 0ae24563f5..ea6a2bf8e6 100644
--- a/spec/components/timeline_lookup_spec.rb
+++ b/spec/components/timeline_lookup_spec.rb
@@ -25,16 +25,19 @@ describe TimelineLookup do
input = (1..100).map { |i| [1000 + i, 100 - i] }
result = TimelineLookup.build(input, 5)
- expect(result.size).to eq(5)
- expect(result).to eq([[1, 99], [21, 79], [41, 59], [61, 39], [81, 19]])
+ # even if max_value is 5 we might get 6 (5 + 1)
+ # to ensure the last tuple is captured
+ expect(result).to eq([[1, 99], [21, 79], [41, 59], [61, 39], [81, 19], [input.size, input.last[1]]])
end
it "respects an uneven `max_values` setting" do
input = (1..100).map { |i| [1000 + i, 100 - i] }
result = TimelineLookup.build(input, 3)
- expect(result.size).to eq(3)
- expect(result).to eq([[1, 99], [35, 65], [69, 31]])
+ # even if max_value is 3 we might get 4 (3 + 1)
+ # to ensure the last tuple is captured
+ expect(result.size).to eq(4)
+ expect(result).to eq([[1, 99], [35, 65], [69, 31], [input.size, input.last[1]]])
end
end
diff --git a/spec/lib/timeline_lookup_spec.rb b/spec/lib/timeline_lookup_spec.rb
deleted file mode 100644
index 517b038c57..0000000000
--- a/spec/lib/timeline_lookup_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe TimelineLookup do
-
- context '.build' do
- it 'keeps the last tuple in the lookup' do
- tuples = [
- [7173, 400], [7174, 390], [7175, 380], [7176, 370], [7177, 1]
- ]
-
- expect(TimelineLookup.build(tuples, 2)).to eq([[1, 400], [4, 370], [5, 1]])
- end
- end
-
-end
From 77c48644eb8183d2c2a7cc7e4b95245b0d8d3d6c Mon Sep 17 00:00:00 2001
From: Dan Ungureanu
Date: Mon, 25 Jan 2021 15:16:21 +0200
Subject: [PATCH 41/85] FIX: Dismissing unread topics with a tag (#11832)
This commits add missing router service to the mixin. It did not work
because 'router' was undefined.
---
.../javascripts/discourse/app/mixins/bulk-topic-selection.js | 3 +++
1 file changed, 3 insertions(+)
diff --git a/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js b/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js
index d539a14063..2908384952 100644
--- a/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js
+++ b/app/assets/javascripts/discourse/app/mixins/bulk-topic-selection.js
@@ -3,8 +3,11 @@ import { NotificationLevels } from "discourse/lib/notification-levels";
import Topic from "discourse/models/topic";
import { alias } from "@ember/object/computed";
import { on } from "discourse-common/utils/decorators";
+import { inject as service } from "@ember/service";
export default Mixin.create({
+ router: service(),
+
bulkSelectEnabled: false,
selected: null,
From e65c5b0aad4c754e0d8b0a19fa6655bba0abcc44 Mon Sep 17 00:00:00 2001
From: Gerhard Schlager
Date: Mon, 25 Jan 2021 14:30:17 +0100
Subject: [PATCH 42/85] PERF: Migrate search data after locale rename (#11831)
The default locale `en_US` has been renamed into `en`. This tries to migrate existing search data to avoid resource intensive reindexing.
---
...search_data_after_default_locale_rename.rb | 28 +++++++++++++++++++
1 file changed, 28 insertions(+)
create mode 100644 db/post_migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb
diff --git a/db/post_migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb b/db/post_migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb
new file mode 100644
index 0000000000..262045c8a2
--- /dev/null
+++ b/db/post_migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class MigrateSearchDataAfterDefaultLocaleRename < ActiveRecord::Migration[6.0]
+ def up
+ move_search_data("category_search_data")
+ move_search_data("post_search_data")
+ move_search_data("tag_search_data")
+ move_search_data("topic_search_data")
+ move_search_data("user_search_data")
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+
+ private
+
+ def move_search_data(table_name)
+ execute <<~SQL
+ UPDATE #{table_name} x
+ SET locale = 'en'
+ WHERE locale = 'en_US'
+ SQL
+ rescue
+ # Probably a unique key constraint violation. A background job might have inserted conflicting data during the UPDATE.
+ # We can safely ignore this error. The ReindexSearch job will eventually fix the data.
+ end
+end
From 2092152b035a787bb7b001b60a8a8be6f30ff940 Mon Sep 17 00:00:00 2001
From: David Taylor
Date: Mon, 25 Jan 2021 13:47:44 +0000
Subject: [PATCH 43/85] FIX: Cleanup authentication_data cookie after login
(#11834)
This cookie is only used during login. Having it persist after that can
cause some unusual behavior, especially for sites with short session
lengths.
We were already deleting the cookie following a new signup, but not for
existing users.
This commit moves the cookie deletion logic out of the erb template, and
adds logic and tests to ensure it is always deleted consistently.
Co-authored-by: Jarek Radosz
---
app/helpers/application_helper.rb | 12 ++++++++++++
app/views/layouts/application.html.erb | 4 ++--
spec/requests/application_controller_spec.rb | 16 +++++++++++++---
3 files changed, 27 insertions(+), 5 deletions(-)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 0b994496dc..0f94822298 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -560,4 +560,16 @@ module ApplicationHelper
end
end
end
+
+ def authentication_data
+ return @authentication_data if defined?(@authentication_data)
+
+ @authentication_data = begin
+ value = cookies[:authentication_data]
+ if value
+ cookies.delete(:authentication_data, path: Discourse.base_path("/"))
+ end
+ current_user ? nil : value
+ end
+ end
end
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 165ece79e1..796a7be265 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -60,8 +60,8 @@
<%= tag.meta id: 'data-discourse-setup', data: client_side_setup_data %>
- <%- if !current_user && (data = cookies.delete(:authentication_data, path: Discourse.base_path("/"))) %>
-
+ <%- if authentication_data %>
+
<%- end %>
diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb
index 5e82551fc2..1ac69118a7 100644
--- a/spec/requests/application_controller_spec.rb
+++ b/spec/requests/application_controller_spec.rb
@@ -104,11 +104,21 @@ RSpec.describe ApplicationController do
end
it 'contains authentication data when cookies exist' do
- COOKIE_DATA = "someauthenticationdata"
- cookies['authentication_data'] = COOKIE_DATA
+ cookie_data = "someauthenticationdata"
+ cookies['authentication_data'] = cookie_data
get '/login'
expect(response.status).to eq(200)
- expect(response.body).to include("data-authentication-data=\"#{COOKIE_DATA }\"")
+ expect(response.body).to include("data-authentication-data=\"#{cookie_data}\"")
+ expect(response.headers["Set-Cookie"]).to include("authentication_data=;") # Delete cookie
+ end
+
+ it 'deletes authentication data cookie even if already authenticated' do
+ sign_in(Fabricate(:user))
+ cookies['authentication_data'] = "someauthenticationdata"
+ get '/'
+ expect(response.status).to eq(200)
+ expect(response.body).not_to include("data-authentication-data=")
+ expect(response.headers["Set-Cookie"]).to include("authentication_data=;") # Delete cookie
end
end
From afe6db5f33ad3c94bb016f869445f6216029b59e Mon Sep 17 00:00:00 2001
From: Roman Rizzi
Date: Mon, 25 Jan 2021 11:07:22 -0300
Subject: [PATCH 44/85] FIX: Destroy associated user api keys when making a
user anonymous. (#11760)
---
app/services/user_anonymizer.rb | 11 ++++++-----
spec/services/user_anonymizer_spec.rb | 13 ++++++++++++-
2 files changed, 18 insertions(+), 6 deletions(-)
diff --git a/app/services/user_anonymizer.rb b/app/services/user_anonymizer.rb
index 472a1eb715..970d1fff42 100644
--- a/app/services/user_anonymizer.rb
+++ b/app/services/user_anonymizer.rb
@@ -59,11 +59,12 @@ class UserAnonymizer
)
end
- @user.user_avatar.try(:destroy)
- @user.single_sign_on_record.try(:destroy)
- @user.oauth2_user_infos.try(:destroy_all)
- @user.user_associated_accounts.try(:destroy_all)
- @user.api_keys.find_each { |x| x.try(:destroy) }
+ @user.user_avatar&.destroy!
+ @user.single_sign_on_record&.destroy!
+ @user.oauth2_user_infos.destroy_all
+ @user.user_associated_accounts.destroy_all
+ @user.api_keys.destroy_all
+ @user.user_api_keys.destroy_all
@user.user_emails.secondary.destroy_all
@user_history = log_action
diff --git a/spec/services/user_anonymizer_spec.rb b/spec/services/user_anonymizer_spec.rb
index 84723680e8..d8ec0597ea 100644
--- a/spec/services/user_anonymizer_spec.rb
+++ b/spec/services/user_anonymizer_spec.rb
@@ -210,12 +210,23 @@ describe UserAnonymizer do
end
it "removes api key" do
- ApiKey.create(user_id: user.id)
+ ApiKey.create!(user_id: user.id)
+
expect { make_anonymous }.to change { ApiKey.count }.by(-1)
+
user.reload
expect(user.api_keys).to be_empty
end
+ it "removes user api key" do
+ user_api_key = Fabricate(:user_api_key, user: user)
+
+ expect { make_anonymous }.to change { UserApiKey.count }.by(-1)
+
+ user.reload
+ expect(user.user_api_keys).to be_empty
+ end
+
context "executes job" do
before do
Jobs.run_immediately!
From c7781f11393b3edd53cc148f28ebce87d2efa426 Mon Sep 17 00:00:00 2001
From: Vinoth Kannan
Date: Mon, 25 Jan 2021 22:19:26 +0530
Subject: [PATCH 45/85] UX: respect `email_editable` site setting in user
activation page. (#11835)
Previously, when both `enable_local_logins` and `email_editable` are disabled still user can change the email in user activation page.
---
.../app/components/activation-controls.js | 6 +++++
.../components/activation-controls.hbs | 10 +++++---
.../components/activation-controls-test.js | 25 +++++++++++++++++++
config/site_settings.yml | 4 ++-
4 files changed, 40 insertions(+), 5 deletions(-)
create mode 100644 app/assets/javascripts/discourse/tests/integration/components/activation-controls-test.js
diff --git a/app/assets/javascripts/discourse/app/components/activation-controls.js b/app/assets/javascripts/discourse/app/components/activation-controls.js
index 15d48468b0..99ed8d2b1b 100644
--- a/app/assets/javascripts/discourse/app/components/activation-controls.js
+++ b/app/assets/javascripts/discourse/app/components/activation-controls.js
@@ -1,4 +1,10 @@
import Component from "@ember/component";
+import { or } from "@ember/object/computed";
+
export default Component.extend({
classNames: "activation-controls",
+ canEditEmail: or(
+ "siteSettings.enable_local_logins",
+ "siteSettings.email_editable"
+ ),
});
diff --git a/app/assets/javascripts/discourse/app/templates/components/activation-controls.hbs b/app/assets/javascripts/discourse/app/templates/components/activation-controls.hbs
index a311180f53..454aa33103 100644
--- a/app/assets/javascripts/discourse/app/templates/components/activation-controls.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/activation-controls.hbs
@@ -5,7 +5,9 @@
class="btn-primary resend"}}
{{/unless}}
-{{d-button action=editActivationEmail
- label="login.change_email"
- icon="pencil-alt"
- class="edit-email"}}
+{{#if canEditEmail}}
+ {{d-button action=editActivationEmail
+ label="login.change_email"
+ icon="pencil-alt"
+ class="edit-email"}}
+{{/if}}
diff --git a/app/assets/javascripts/discourse/tests/integration/components/activation-controls-test.js b/app/assets/javascripts/discourse/tests/integration/components/activation-controls-test.js
new file mode 100644
index 0000000000..d44c398158
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/integration/components/activation-controls-test.js
@@ -0,0 +1,25 @@
+import componentTest, {
+ setupRenderingTest,
+} from "discourse/tests/helpers/component-test";
+import {
+ discourseModule,
+ queryAll,
+} from "discourse/tests/helpers/qunit-helpers";
+
+discourseModule("Integration | Component | activation-controls", function (
+ hooks
+) {
+ setupRenderingTest(hooks);
+
+ componentTest("hides change email button", {
+ template: `{{activation-controls}}`,
+ beforeEach() {
+ this.siteSettings.enable_local_logins = false;
+ this.siteSettings.email_editable = false;
+ },
+
+ test(assert) {
+ assert.equal(queryAll("button.edit-email").length, 0);
+ },
+ });
+});
diff --git a/config/site_settings.yml b/config/site_settings.yml
index d32b2ebc49..6834aed5bb 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -541,7 +541,9 @@ users:
max: 10
block_common_passwords: true
username_change_period: 3
- email_editable: true
+ email_editable:
+ client: true
+ default: true
logout_redirect:
client: true
default: ""
From d364d4827c0d33e2090f6e54078c35a463a119be Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 25 Jan 2021 13:25:40 -0500
Subject: [PATCH 46/85] Build(deps): Bump mini_suffix from 0.3.0 to 0.3.2
(#11839)
Bumps [mini_suffix](https://github.com/discourse/mini_suffix) from 0.3.0 to 0.3.2.
- [Release notes](https://github.com/discourse/mini_suffix/releases)
- [Changelog](https://github.com/discourse/mini_suffix/blob/master/CHANGELOG.md)
- [Commits](https://github.com/discourse/mini_suffix/commits/v0.3.2)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Gemfile.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 898a0c2bb7..3e701c0873 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -205,7 +205,7 @@ GEM
mini_scheduler (0.13.0)
sidekiq (>= 4.2.3)
mini_sql (1.0.1)
- mini_suffix (0.3.0)
+ mini_suffix (0.3.2)
ffi (~> 1.9)
minitest (5.14.3)
mocha (1.12.0)
From 27656f5c845721af851baa3ca0bdebcf085bf128 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?=
Date: Mon, 25 Jan 2021 20:33:11 +0100
Subject: [PATCH 47/85] FIX: un-prioritise inactive users in user search
(#11838)
When doing a user search (eg. when mentioning a user) we will not prioritie
users who hasn't been seen in over a year.
REFACTOR the user-search specs to be more precise regarding the ordering
---
app/models/user_search.rb | 3 +-
spec/models/user_search_spec.rb | 203 ++++++++++++++++----------------
2 files changed, 104 insertions(+), 102 deletions(-)
diff --git a/app/models/user_search.rb b/app/models/user_search.rb
index 2b9b05d0f8..aafc0d3e81 100644
--- a/app/models/user_search.rb
+++ b/app/models/user_search.rb
@@ -64,10 +64,11 @@ class UserSearch
def search_ids
users = Set.new
- # 1. exact username matches
+ # 1. exact username matches active in the past year
if @term.present?
scoped_users
.where(username_lower: @term)
+ .where('last_seen_at > ?', 1.year.ago)
.limit(@limit)
.pluck(:id)
.each { |id| users << id }
diff --git a/spec/models/user_search_spec.rb b/spec/models/user_search_spec.rb
index 5dad83c32b..d631d59d97 100644
--- a/spec/models/user_search_spec.rb
+++ b/spec/models/user_search_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'rails_helper'
+require "rails_helper"
describe UserSearch do
@@ -11,170 +11,163 @@ describe UserSearch do
fab!(:topic2) { Fabricate :topic }
fab!(:topic3) { Fabricate :topic }
fab!(:topic4) { Fabricate :topic }
- fab!(:user1) { Fabricate :user, username: "mrb", name: "Michael Madsen", last_seen_at: 10.days.ago }
- fab!(:user2) { Fabricate :user, username: "mrblue", name: "Eddie Code", last_seen_at: 9.days.ago }
- fab!(:user3) { Fabricate :user, username: "mrorange", name: "Tim Roth", last_seen_at: 8.days.ago }
- fab!(:user4) { Fabricate :user, username: "mrpink", name: "Steve Buscemi", last_seen_at: 7.days.ago }
- fab!(:user5) { Fabricate :user, username: "mrbrown", name: "Quentin Tarantino", last_seen_at: 6.days.ago }
- fab!(:user6) { Fabricate :user, username: "mrwhite", name: "Harvey Keitel", last_seen_at: 5.days.ago }
- fab!(:inactive) { Fabricate :user, username: "Ghost", active: false }
+ fab!(:mr_b) { Fabricate :user, username: "mrb", name: "Michael Madsen", last_seen_at: 10.days.ago }
+ fab!(:mr_blue) { Fabricate :user, username: "mrblue", name: "Eddie Code", last_seen_at: 9.days.ago }
+ fab!(:mr_orange) { Fabricate :user, username: "mrorange", name: "Tim Roth", last_seen_at: 8.days.ago }
+ fab!(:mr_pink) { Fabricate :user, username: "mrpink", name: "Steve Buscemi", last_seen_at: 7.days.ago }
+ fab!(:mr_brown) { Fabricate :user, username: "mrbrown", name: "Quentin Tarantino", last_seen_at: 6.days.ago }
+ fab!(:mr_white) { Fabricate :user, username: "mrwhite", name: "Harvey Keitel", last_seen_at: 5.days.ago }
+ fab!(:inactive) { Fabricate :user, username: "Ghost", active: false }
fab!(:admin) { Fabricate :admin, username: "theadmin" }
fab!(:moderator) { Fabricate :moderator, username: "themod" }
fab!(:staged) { Fabricate :staged }
def search_for(*args)
- UserSearch.new(*args).search
+ # mapping "username" so it's easier to debug
+ UserSearch.new(*args).search.map(&:username)
end
- context 'with a secure category' do
- fab!(:group) { Fabricate :group }
- fab!(:user) { Fabricate :user }
- fab!(:searching_user) { Fabricate :user }
+ context "with a secure category" do
+ fab!(:user) { Fabricate(:user) }
+ fab!(:searching_user) { Fabricate(:user) }
+ fab!(:group) { Fabricate(:group) }
+ fab!(:category) { Fabricate(:category, read_restricted: true, user: user) }
+
before_all do
+ Fabricate(:category_group, category: category, group: group)
+
group.add(user)
group.add(searching_user)
group.save
end
- fab!(:category) { Fabricate(:category,
- read_restricted: true,
- user: user)
- }
- before_all { Fabricate(:category_group, category: category, group: group) }
- it 'autocompletes with people in the category' do
+ it "autocompletes with people in the category" do
results = search_for("", searching_user: searching_user, category_id: category.id)
-
- expect(user.username).to eq(results[0].username)
- expect(results.length).to eq(1)
+ expect(results).to eq [user.username]
end
- it 'will lookup the category from the topic id' do
+ it "will lookup the category from the topic id" do
topic = Fabricate(:topic, category: category)
- _post = Fabricate(:post, user: topic.user, topic: topic)
+ Fabricate(:post, user: topic.user, topic: topic)
results = search_for("", searching_user: searching_user, topic_id: topic.id)
- expect(results.length).to eq(2)
-
- expect(results.map(&:username)).to contain_exactly(
- user.username, topic.user.username
- )
+ expect(results).to eq [topic.user, user].map(&:username)
end
- it 'will raise an error if the user cannot see the category' do
+ it "will raise an error if the user cannot see the category" do
expect do
search_for("", searching_user: Fabricate(:user), category_id: category.id)
end.to raise_error(Discourse::InvalidAccess)
end
- it 'will respect the group member visibility setting' do
+ it "will respect the group member visibility setting" do
group.update(members_visibility_level: Group.visibility_levels[:owners])
results = search_for("", searching_user: searching_user, category_id: category.id)
- expect(results.length).to eq(0)
+ expect(results).to be_blank
group.add_owner(searching_user)
results = search_for("", searching_user: searching_user, category_id: category.id)
- expect(results.length).to eq(1)
+ expect(results).to eq [user.username]
end
end
- it 'allows for correct underscore searching' do
- Fabricate(:user, username: 'Under_Score')
- Fabricate(:user, username: 'undertaker')
+ it "allows for correct underscore searching" do
+ Fabricate(:user, username: "undertaker")
+ under_score = Fabricate(:user, username: "Under_Score")
- expect(search_for("under_sc").length).to eq(1)
- expect(search_for("under_").length).to eq(1)
+ expect(search_for("under_sc")).to eq [under_score.username]
+ expect(search_for("under_")).to eq [under_score.username]
end
- it 'allows filtering by group' do
+ it "allows filtering by group" do
+ sam = Fabricate(:user, username: "sam")
+ Fabricate(:user, username: "samantha")
+
group = Fabricate(:group)
- sam = Fabricate(:user, username: 'sam')
- _samantha = Fabricate(:user, username: 'samantha')
group.add(sam)
results = search_for("sam", groups: [group])
- expect(results.count).to eq(1)
+ expect(results).to eq [sam.username]
end
- it 'allows filtering by multiple groups' do
+ it "allows filtering by multiple groups" do
+ sam = Fabricate(:user, username: "sam")
+ samantha = Fabricate(:user, username: "samantha")
+
group_1 = Fabricate(:group)
- sam = Fabricate(:user, username: 'sam')
- group_2 = Fabricate(:group)
- samantha = Fabricate(:user, username: 'samantha')
group_1.add(sam)
+
+ group_2 = Fabricate(:group)
group_2.add(samantha)
results = search_for("sam", groups: [group_1, group_2])
- expect(results.count).to eq(2)
+ expect(results).to eq [sam, samantha].map(&:username)
end
context "with seed data" do
- fab!(:post1) { Fabricate :post, user: user1, topic: topic }
- fab!(:post2) { Fabricate :post, user: user2, topic: topic2 }
- fab!(:post3) { Fabricate :post, user: user3, topic: topic }
- fab!(:post4) { Fabricate :post, user: user4, topic: topic }
- fab!(:post5) { Fabricate :post, user: user5, topic: topic3 }
- fab!(:post6) { Fabricate :post, user: user6, topic: topic }
+ fab!(:post1) { Fabricate :post, user: mr_b, topic: topic }
+ fab!(:post2) { Fabricate :post, user: mr_blue, topic: topic2 }
+ fab!(:post3) { Fabricate :post, user: mr_orange, topic: topic }
+ fab!(:post4) { Fabricate :post, user: mr_pink, topic: topic }
+ fab!(:post5) { Fabricate :post, user: mr_brown, topic: topic3 }
+ fab!(:post6) { Fabricate :post, user: mr_white, topic: topic }
fab!(:post7) { Fabricate :post, user: staged, topic: topic4 }
- before { user6.update(suspended_at: 1.day.ago, suspended_till: 1.year.from_now) }
+ before { mr_white.update(suspended_at: 1.day.ago, suspended_till: 1.year.from_now) }
it "can search by name and username" do
# normal search
- results = search_for(user1.name.split(" ").first)
- expect(results.size).to eq(1)
- expect(results.first.username).to eq(user1.username)
+ results = search_for(mr_b.name.split.first)
+ expect(results).to eq [mr_b.username]
# lower case
- results = search_for(user1.name.split(" ").first.downcase)
- expect(results.size).to eq(1)
- expect(results.first).to eq(user1)
+ results = search_for(mr_b.name.split.first.downcase)
+ expect(results).to eq [mr_b.username]
# username
- results = search_for(user4.username)
- expect(results.size).to eq(1)
- expect(results.first).to eq(user4)
+ results = search_for(mr_pink.username)
+ expect(results).to eq [mr_pink.username]
# case insensitive
- results = search_for(user4.username.upcase)
- expect(results.size).to eq(1)
- expect(results.first).to eq(user4)
+ results = search_for(mr_pink.username.upcase)
+ expect(results).to eq [mr_pink.username]
end
it "handles substring search correctly" do
- # substrings
- # only staff members see suspended users in results
results = search_for("mr")
- expect(results.size).to eq(5)
- expect(results).not_to include(user6)
- expect(search_for("mr", searching_user: user1).size).to eq(5)
+ expect(results).to eq [mr_brown, mr_pink, mr_orange, mr_blue, mr_b].map(&:username)
+
+ results = search_for("mr", searching_user: mr_b)
+ expect(results).to eq [mr_brown, mr_pink, mr_orange, mr_blue, mr_b].map(&:username)
+
+ # only staff members see suspended users in results
+ results = search_for("mr", searching_user: moderator)
+ expect(results).to eq [mr_white, mr_brown, mr_pink, mr_orange, mr_blue, mr_b].map(&:username)
results = search_for("mr", searching_user: admin)
- expect(results.size).to eq(6)
- expect(results).to include(user6)
- expect(search_for("mr", searching_user: moderator).size).to eq(6)
+ expect(results).to eq [mr_white, mr_brown, mr_pink, mr_orange, mr_blue, mr_b].map(&:username)
- results = search_for(user1.username, searching_user: admin)
- expect(results.size).to eq(3)
+ results = search_for(mr_b.username, searching_user: admin)
+ expect(results).to eq [mr_b, mr_brown, mr_blue].map(&:username)
results = search_for("MR", searching_user: admin)
- expect(results.size).to eq(6)
+ expect(results).to eq [mr_white, mr_brown, mr_pink, mr_orange, mr_blue, mr_b].map(&:username)
results = search_for("MRB", searching_user: admin, limit: 2)
- expect(results.size).to eq(2)
+ expect(results).to eq [mr_b, mr_brown].map(&:username)
end
it "prioritises topic participants" do
- # topic priority
- results = search_for(user1.username, topic_id: topic.id)
- expect(results.first).to eq(user1)
+ results = search_for(mr_b.username, topic_id: topic.id)
+ expect(results).to eq [mr_b, mr_brown, mr_blue].map(&:username)
- results = search_for(user1.username, topic_id: topic2.id)
- expect(results[1]).to eq(user2)
+ results = search_for(mr_b.username, topic_id: topic2.id)
+ expect(results).to eq [mr_b, mr_blue, mr_brown].map(&:username)
- results = search_for(user1.username, topic_id: topic3.id)
- expect(results[1]).to eq(user5)
+ results = search_for(mr_b.username, topic_id: topic3.id)
+ expect(results).to eq [mr_b, mr_brown, mr_blue].map(&:username)
end
it "only reveals topic participants to people with permission" do
@@ -187,37 +180,46 @@ describe UserSearch do
# Random user, does not have access
expect do
- search_for("", topic_id: pm_topic.id, searching_user: user1)
+ search_for("", topic_id: pm_topic.id, searching_user: mr_b)
end.to raise_error(Discourse::InvalidAccess)
- pm_topic.invite(pm_topic.user, user1.username)
- results = search_for("", topic_id: pm_topic.id, searching_user: user1)
- expect(results.length).to eq(1)
- expect(results[0]).to eq(pm_topic.user)
+ pm_topic.invite(pm_topic.user, mr_b.username)
+
+ results = search_for("", topic_id: pm_topic.id, searching_user: mr_b)
+ expect(results).to eq [pm_topic.user.username]
end
it "only searches by name when enabled" do
# When searching by name is enabled, it returns the record
SiteSetting.enable_names = true
results = search_for("Tarantino")
- expect(results.size).to eq(1)
+ expect(results).to eq [mr_brown.username]
results = search_for("coding")
- expect(results.size).to eq(0)
+ expect(results).to be_blank
results = search_for("z")
- expect(results.size).to eq(0)
+ expect(results).to be_blank
# When searching by name is disabled, it will not return the record
SiteSetting.enable_names = false
results = search_for("Tarantino")
- expect(results.size).to eq(0)
+ expect(results).to be_blank
end
it "prioritises exact matches" do
- # find an exact match first
results = search_for("mrB")
- expect(results.first.username).to eq(user1.username)
+ expect(results).to eq [mr_b, mr_brown, mr_blue].map(&:username)
+ end
+
+ it "doesn't prioritises exact matches for users who haven't been seen in more than 1 year" do
+ abcdef = Fabricate(:user, username: "abcdef", last_seen_at: 2.days.ago)
+ abcde = Fabricate(:user, username: "abcde", last_seen_at: 2.weeks.ago)
+ abcd = Fabricate(:user, username: "abcd", last_seen_at: 2.months.ago)
+ abc = Fabricate(:user, username: "abc", last_seen_at: 2.years.ago)
+
+ results = search_for("abc")
+ expect(results).to eq [abcdef, abcde, abcd, abc].map(&:username)
end
it "does not include self, staged or inactive" do
@@ -230,12 +232,11 @@ describe UserSearch do
expect(results).to be_blank
results = search_for(staged.username, include_staged_users: true)
- expect(results.first.username).to eq(staged.username)
+ expect(results).to eq [staged.username]
- results = search_for("", topic_id: topic.id, searching_user: user1)
-
- # mrb is omitted, mrb is current user
- expect(results.map(&:username)).to eq(["mrpink", "mrorange"])
+ # mrb is omitted since they're the searching user
+ results = search_for("", topic_id: topic.id, searching_user: mr_b)
+ expect(results).to eq [mr_pink, mr_orange].map(&:username)
end
end
end
From f421d9bdd6a85d6df907848d8ad35c758ef636ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?=
Date: Mon, 25 Jan 2021 21:27:15 +0100
Subject: [PATCH 48/85] FIX: only de-prioritise exact matches in mentions
(#11843)
Not when doing a site-wide search like we do in the Directory.
This solves the following specfailure:
1) DirectoryItemsController with data finds user by name
Failure/Error: expect(json['directory_items'].length).to eq(1)
expected: 1
got: 0
(compared using ==)
# ./spec/requests/directory_items_controller_spec.rb:88:in `block (3 levels) in '
# ./spec/rails_helper.rb:271:in `block (2 levels) in '
# ./bundle/ruby/2.7.0/gems/webmock-3.11.1/lib/webmock/rspec.rb:37:in `block (2 levels) in '
---
app/models/user_search.rb | 11 +++++++----
spec/models/user_search_spec.rb | 4 ++--
2 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/app/models/user_search.rb b/app/models/user_search.rb
index aafc0d3e81..7498b230be 100644
--- a/app/models/user_search.rb
+++ b/app/models/user_search.rb
@@ -64,11 +64,14 @@ class UserSearch
def search_ids
users = Set.new
- # 1. exact username matches active in the past year
+ # 1. exact username matches
if @term.present?
- scoped_users
- .where(username_lower: @term)
- .where('last_seen_at > ?', 1.year.ago)
+ exact_matches = scoped_users.where(username_lower: @term)
+
+ # don't polute mentions with users who haven't shown up in over a year
+ exact_matches = exact_matches.where('last_seen_at > ?', 1.year.ago) if @topic_id || @category_id
+
+ exact_matches
.limit(@limit)
.pluck(:id)
.each { |id| users << id }
diff --git a/spec/models/user_search_spec.rb b/spec/models/user_search_spec.rb
index d631d59d97..c72abc8468 100644
--- a/spec/models/user_search_spec.rb
+++ b/spec/models/user_search_spec.rb
@@ -212,13 +212,13 @@ describe UserSearch do
expect(results).to eq [mr_b, mr_brown, mr_blue].map(&:username)
end
- it "doesn't prioritises exact matches for users who haven't been seen in more than 1 year" do
+ it "doesn't prioritises exact matches mentions for users who haven't been seen in over a year" do
abcdef = Fabricate(:user, username: "abcdef", last_seen_at: 2.days.ago)
abcde = Fabricate(:user, username: "abcde", last_seen_at: 2.weeks.ago)
abcd = Fabricate(:user, username: "abcd", last_seen_at: 2.months.ago)
abc = Fabricate(:user, username: "abc", last_seen_at: 2.years.ago)
- results = search_for("abc")
+ results = search_for("abc", topic_id: topic.id)
expect(results).to eq [abcdef, abcde, abcd, abc].map(&:username)
end
From 363dca5ddc79f92d4b405d2fbfe9d8a1ece5c496 Mon Sep 17 00:00:00 2001
From: Gerhard Schlager
Date: Mon, 25 Jan 2021 21:45:13 +0100
Subject: [PATCH 49/85] FIX: "Customize text" link was broken on badges admin
page (#11842)
---
.../javascripts/admin/addon/templates/badges-show.hbs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/assets/javascripts/admin/addon/templates/badges-show.hbs b/app/assets/javascripts/admin/addon/templates/badges-show.hbs
index 82057b103c..429f0cbc94 100644
--- a/app/assets/javascripts/admin/addon/templates/badges-show.hbs
+++ b/app/assets/javascripts/admin/addon/templates/badges-show.hbs
@@ -5,7 +5,7 @@
{{#if readOnly}}
{{input type="text" name="name" value=buffered.name disabled=true}}
- {{#link-to "adminSiteText.edit" (concat textCustomizationPrefix "name")}}
+ {{#link-to "adminSiteText" (query-params q=(concat textCustomizationPrefix "name"))}}
{{i18n "admin.badges.read_only_setting_help"}}
{{/link-to}}
@@ -70,7 +70,7 @@
{{#if buffered.system}}
{{textarea name="description" value=buffered.description disabled=true}}
- {{#link-to "adminSiteText.edit" (concat textCustomizationPrefix "description")}}
+ {{#link-to "adminSiteText" (query-params q=(concat textCustomizationPrefix "description"))}}
{{i18n "admin.badges.read_only_setting_help"}}
{{/link-to}}
@@ -84,7 +84,7 @@
{{#if buffered.system}}
{{textarea name="long_description" value=buffered.long_description disabled=true}}
- {{#link-to "adminSiteText.edit" (concat textCustomizationPrefix "long_description")}}
+ {{#link-to "adminSiteText" (query-params q=(concat textCustomizationPrefix "long_description"))}}
{{i18n "admin.badges.read_only_setting_help"}}
{{/link-to}}
From 568bad75c19279f35fcba72eecfaeeda909f83e4 Mon Sep 17 00:00:00 2001
From: Penar Musaraj
Date: Mon, 25 Jan 2021 16:30:43 -0500
Subject: [PATCH 50/85] FIX: Support version in new feature payload (#11841)
Followup to 4f01ca87e30c4b7d63a71267e4a572eea786c152
---
lib/discourse_updates.rb | 4 ++++
spec/components/discourse_updates_spec.rb | 20 ++++++++++++++++++++
2 files changed, 24 insertions(+)
diff --git a/lib/discourse_updates.rb b/lib/discourse_updates.rb
index d5d2f3b264..a0274fe092 100644
--- a/lib/discourse_updates.rb
+++ b/lib/discourse_updates.rb
@@ -131,6 +131,10 @@ module DiscourseUpdates
entries.select! { |item| Time.zone.parse(item["created_at"]) > last_seen }
end
+ entries.select! do |item|
+ item["discourse_version"].nil? || Discourse.has_needed_version?(Discourse::VERSION::STRING, item["discourse_version"]) rescue nil
+ end
+
entries.sort { |item| Time.zone.parse(item["created_at"]) }
end
diff --git a/spec/components/discourse_updates_spec.rb b/spec/components/discourse_updates_spec.rb
index e3f911ca6f..41df256863 100644
--- a/spec/components/discourse_updates_spec.rb
+++ b/spec/components/discourse_updates_spec.rb
@@ -211,5 +211,25 @@ describe DiscourseUpdates do
expect(result.length).to eq(1)
expect(result[0]["title"]).to eq("Brand New Item")
end
+
+ it 'correctly shows features by Discourse version' do
+ features_with_versions = [
+ { "emoji" => "🤾", "title" => "Bells", "created_at" => 40.minutes.ago },
+ { "emoji" => "🙈", "title" => "Whistles", "created_at" => 20.minutes.ago, discourse_version: "2.6.0.beta1" },
+ { "emoji" => "🙈", "title" => "Confetti", "created_at" => 15.minutes.ago, discourse_version: "2.7.0.beta2" },
+ { "emoji" => "🤾", "title" => "Not shown yet", "created_at" => 10.minutes.ago, discourse_version: "2.7.0.beta5" },
+ { "emoji" => "🤾", "title" => "Not shown yet (beta < stable)", "created_at" => 10.minutes.ago, discourse_version: "2.7.0" },
+ { "emoji" => "🤾", "title" => "Ignore invalid version", "created_at" => 10.minutes.ago, discourse_version: "invalid-version" },
+ ]
+
+ Discourse.redis.set('new_features', MultiJson.dump(features_with_versions))
+ DiscourseUpdates.stubs(:last_installed_version).returns("2.7.0.beta2")
+ result = DiscourseUpdates.unseen_new_features(admin.id)
+
+ expect(result.length).to eq(3)
+ expect(result[0]["title"]).to eq("Confetti")
+ expect(result[1]["title"]).to eq("Whistles")
+ expect(result[2]["title"]).to eq("Bells")
+ end
end
end
From c56ba6c9bdefb85dc85c5b627371bc5dd61d122b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?=
Date: Mon, 25 Jan 2021 22:58:58 +0100
Subject: [PATCH 51/85] PERF: batch expensive post-migration (#11845)
Run the 'MigrateSearchDataAfterDefaultLocaleRename' post migration in batches of 500k records.
This will hopefully prevent any potential deadlocks on large tables.
---
...search_data_after_default_locale_rename.rb | 35 ++++++++++++-------
1 file changed, 22 insertions(+), 13 deletions(-)
diff --git a/db/post_migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb b/db/post_migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb
index 262045c8a2..407dfd8bb5 100644
--- a/db/post_migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb
+++ b/db/post_migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
class MigrateSearchDataAfterDefaultLocaleRename < ActiveRecord::Migration[6.0]
+ disable_ddl_transaction!
+
def up
- move_search_data("category_search_data")
- move_search_data("post_search_data")
- move_search_data("tag_search_data")
- move_search_data("topic_search_data")
- move_search_data("user_search_data")
+ %w{category tag topic post user}.each { |model| fix_search_data(model) }
end
def down
@@ -15,14 +13,25 @@ class MigrateSearchDataAfterDefaultLocaleRename < ActiveRecord::Migration[6.0]
private
- def move_search_data(table_name)
- execute <<~SQL
- UPDATE #{table_name} x
- SET locale = 'en'
- WHERE locale = 'en_US'
+ def fix_search_data(model)
+ key = "#{model}_id"
+ table = "#{model}_search_data"
+
+ sql = <<~SQL
+ UPDATE #{table}
+ SET locale = 'en'
+ WHERE #{key} IN (
+ SELECT #{key}
+ FROM #{table}
+ WHERE locale = 'en_US'
+ LIMIT 500000
+ )
SQL
- rescue
- # Probably a unique key constraint violation. A background job might have inserted conflicting data during the UPDATE.
- # We can safely ignore this error. The ReindexSearch job will eventually fix the data.
+
+ loop do
+ count = execute(sql).cmd_tuples
+ break if count == 0
+ puts "Migrated #{count} rows of #{table} to new locale."
+ end
end
end
From 67db5e97f8d195dc911e50c4b7cabd559414a019 Mon Sep 17 00:00:00 2001
From: David Taylor
Date: Tue, 26 Jan 2021 11:32:43 +0000
Subject: [PATCH 52/85] FEATURE: Add extra response headers to nginx log format
(#11840)
These headers are useful for debugging and performance analysis
---
config/nginx.sample.conf | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/config/nginx.sample.conf b/config/nginx.sample.conf
index 6fd053d5d9..b417c85388 100644
--- a/config/nginx.sample.conf
+++ b/config/nginx.sample.conf
@@ -29,7 +29,7 @@ map $http_x_forwarded_proto $thescheme {
https https;
}
-log_format log_discourse '[$time_local] "$http_host" $remote_addr "$request" "$http_user_agent" "$sent_http_x_discourse_route" $status $bytes_sent "$http_referer" $upstream_response_time $request_time "$sent_http_x_discourse_username"';
+log_format log_discourse '[$time_local] "$http_host" $remote_addr "$request" "$http_user_agent" "$sent_http_x_discourse_route" $status $bytes_sent "$http_referer" $upstream_response_time $request_time "$sent_http_x_discourse_username" "$sent_http_x_discourse_trackview" "$sent_http_x_queue_time" "$sent_http_x_redis_calls" "$sent_http_x_redis_time" "$sent_http_x_sql_calls" "$sent_http_x_sql_time"';
server {
From 4d70cc379b05dc97da94653f871304b3d1abeca0 Mon Sep 17 00:00:00 2001
From: Dan Ungureanu
Date: Tue, 26 Jan 2021 14:44:00 +0200
Subject: [PATCH 53/85] DEV: Add test (#11847)
Follow-up to 77c48644eb8183d2c2a7cc7e4b95245b0d8d3d6c.
---
.../discourse/tests/acceptance/tags-test.js | 76 +++++++++++++++++++
1 file changed, 76 insertions(+)
diff --git a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js
index 31b22fc653..434e21cba1 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js
@@ -3,6 +3,7 @@ import {
exists,
queryAll,
updateCurrentUser,
+ invisible,
} from "discourse/tests/helpers/qunit-helpers";
import { click, currentURL, visit } from "@ember/test-helpers";
import { test } from "qunit";
@@ -10,6 +11,74 @@ import { test } from "qunit";
acceptance("Tags", function (needs) {
needs.user();
+ needs.pretender((server, helper) => {
+ server.get("/tag/test/notifications", () =>
+ helper.response({
+ tag_notification: { id: "test", notification_level: 2 },
+ })
+ );
+
+ server.get("/tag/test/l/unread.json", () =>
+ helper.response({
+ users: [
+ {
+ id: 42,
+ username: "foo",
+ name: "Foo",
+ avatar_template: "/user_avatar/localhost/foo/{size}/10265_2.png",
+ },
+ ],
+ primary_groups: [],
+ topic_list: {
+ can_create_topic: true,
+ draft: null,
+ draft_key: "new_topic",
+ per_page: 30,
+ top_tags: [],
+ tags: [{ id: 42, name: "test", topic_count: 1, staff: false }],
+ topics: [
+ {
+ id: 42,
+ title: "Hello world",
+ fancy_title: "Hello world",
+ slug: "hello-world",
+ posts_count: 1,
+ reply_count: 1,
+ highest_post_number: 1,
+ created_at: "2020-01-01T00:00:00.000Z",
+ last_posted_at: "2020-01-01T00:00:00.000Z",
+ bumped: true,
+ bumped_at: "2020-01-01T00:00:00.000Z",
+ archetype: "regular",
+ unseen: false,
+ last_read_post_number: 1,
+ unread: 0,
+ new_posts: 1,
+ pinned: false,
+ unpinned: null,
+ visible: true,
+ closed: true,
+ archived: false,
+ notification_level: 3,
+ bookmarked: false,
+ liked: true,
+ tags: ["test"],
+ views: 42,
+ like_count: 42,
+ has_summary: false,
+ last_poster_username: "foo",
+ pinned_globally: false,
+ featured_link: null,
+ posters: [],
+ },
+ ],
+ },
+ })
+ );
+
+ server.put("/topics/bulk", () => helper.response({}));
+ });
+
test("list the tags", async function (assert) {
await visit("/tags");
@@ -19,6 +88,13 @@ acceptance("Tags", function (needs) {
"shows the eviltrout tag"
);
});
+
+ test("dismiss notifications", async function (assert) {
+ await visit("/tag/test/l/unread");
+ await click("button.dismiss-read");
+ await click(".dismiss-read-modal button.btn-primary");
+ assert.ok(invisible(".dismiss-read-modal"));
+ });
});
acceptance("Tags listed by group", function (needs) {
From 3c028cb67fbed7b51d9428232ee4a9ab65196126 Mon Sep 17 00:00:00 2001
From: Discourse Translator Bot
Date: Tue, 26 Jan 2021 14:52:35 +0100
Subject: [PATCH 54/85] Update translations (#11848)
---
config/locales/client.ar.yml | 2 -
config/locales/client.bs_BA.yml | 2 -
config/locales/client.ca.yml | 2 -
config/locales/client.da.yml | 2 -
config/locales/client.de.yml | 25 +++-
config/locales/client.el.yml | 2 -
config/locales/client.en_GB.yml | 11 --
config/locales/client.es.yml | 58 +++++++---
config/locales/client.fi.yml | 2 -
config/locales/client.fr.yml | 33 +++---
config/locales/client.gl.yml | 46 ++++++--
config/locales/client.he.yml | 25 ++--
config/locales/client.hu.yml | 82 +++++++-------
config/locales/client.hy.yml | 2 -
config/locales/client.it.yml | 19 +---
config/locales/client.ja.yml | 10 +-
config/locales/client.ko.yml | 38 ++++---
config/locales/client.nl.yml | 13 ---
config/locales/client.pl_PL.yml | 107 ++++++++++--------
config/locales/client.pt_BR.yml | 4 +-
config/locales/client.ru.yml | 20 +++-
config/locales/client.sl.yml | 2 -
config/locales/client.sv.yml | 17 ++-
config/locales/client.tr_TR.yml | 17 ++-
config/locales/client.uk.yml | 13 ---
config/locales/client.ur.yml | 2 -
config/locales/client.vi.yml | 13 ---
config/locales/client.zh_CN.yml | 33 ++++--
config/locales/server.de.yml | 4 +
config/locales/server.es.yml | 24 ++++
config/locales/server.fr.yml | 2 +-
config/locales/server.gl.yml | 9 ++
config/locales/server.it.yml | 14 +--
config/locales/server.ko.yml | 2 +
config/locales/server.pl_PL.yml | 10 +-
.../config/locales/server.ar.yml | 1 -
.../config/locales/server.be.yml | 1 -
.../config/locales/server.bs_BA.yml | 1 -
.../config/locales/server.ca.yml | 1 -
.../config/locales/server.cs.yml | 1 -
.../config/locales/server.da.yml | 1 -
.../config/locales/server.de.yml | 2 +-
.../config/locales/server.el.yml | 1 -
.../config/locales/server.en_GB.yml | 4 +
.../config/locales/server.es.yml | 1 -
.../config/locales/server.fa_IR.yml | 1 -
.../config/locales/server.fi.yml | 1 -
.../config/locales/server.fr.yml | 1 -
.../config/locales/server.gl.yml | 1 -
.../config/locales/server.he.yml | 2 +-
.../config/locales/server.hu.yml | 1 -
.../config/locales/server.hy.yml | 1 -
.../config/locales/server.id.yml | 1 -
.../config/locales/server.it.yml | 1 -
.../config/locales/server.ja.yml | 1 -
.../config/locales/server.ko.yml | 2 +-
.../config/locales/server.nb_NO.yml | 1 -
.../config/locales/server.nl.yml | 1 -
.../config/locales/server.pl_PL.yml | 2 +-
.../config/locales/server.pt.yml | 1 -
.../config/locales/server.pt_BR.yml | 1 -
.../config/locales/server.ru.yml | 2 +-
.../config/locales/server.sk.yml | 1 -
.../config/locales/server.sl.yml | 1 -
.../config/locales/server.sv.yml | 2 +-
.../config/locales/server.sw.yml | 1 -
.../config/locales/server.tr_TR.yml | 1 -
.../config/locales/server.uk.yml | 1 -
.../config/locales/server.ur.yml | 1 -
.../config/locales/server.vi.yml | 1 -
.../config/locales/server.zh_CN.yml | 2 +-
.../config/locales/client.hu.yml | 6 +
.../styleguide/config/locales/client.hu.yml | 22 ++++
.../styleguide/config/locales/server.hu.yml | 5 +-
public/422.en_GB.html | 2 +-
75 files changed, 437 insertions(+), 311 deletions(-)
diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml
index 32df70d8cb..b2338abe47 100644
--- a/config/locales/client.ar.yml
+++ b/config/locales/client.ar.yml
@@ -1873,8 +1873,6 @@ ar:
date_time_picker:
from: من
to: إلى
- errors:
- to_before_from: "يجب أن يكون تاريخ الانتهاء متأخراً عن التاريخ."
emoji_picker:
filter_placeholder: ابحث عن إيموجي
smileys_&_emotion: الابتسامات والعواطف
diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml
index 793db26e22..d5ae6e75e0 100644
--- a/config/locales/client.bs_BA.yml
+++ b/config/locales/client.bs_BA.yml
@@ -1614,8 +1614,6 @@ bs_BA:
date_time_picker:
from: Od
to: Ka
- errors:
- to_before_from: "Ciljani datum mora biti kasniji od početnog datuma."
emoji_picker:
filter_placeholder: Pretraži emotikone
smileys_&_emotion: Smeješci i osječaj
diff --git a/config/locales/client.ca.yml b/config/locales/client.ca.yml
index 88aad82005..462344ec75 100644
--- a/config/locales/client.ca.yml
+++ b/config/locales/client.ca.yml
@@ -1479,8 +1479,6 @@ ca:
date_time_picker:
from: De
to: A
- errors:
- to_before_from: "La data final ha de ser posterior a la data inicial."
emoji_picker:
filter_placeholder: Cerca un emoji
smileys_&_emotion: Emoticones
diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml
index 8d049b6f4a..d69faf7ea1 100644
--- a/config/locales/client.da.yml
+++ b/config/locales/client.da.yml
@@ -1390,8 +1390,6 @@ da:
date_time_picker:
from: Fra
to: Til
- errors:
- to_before_from: "Til dato skal være senere end fra dato."
emoji_picker:
filter_placeholder: Søg efter humørikon
smileys_&_emotion: Smilys og følelser
diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml
index 32da447236..702cdd1c25 100644
--- a/config/locales/client.de.yml
+++ b/config/locales/client.de.yml
@@ -187,6 +187,7 @@ de:
us_gov_west_1: "AWS GovCloud (US-West)"
us_west_1: "USA West (Nordkalifornien)"
us_west_2: "USA West (Oregon)"
+ clear_input: "Eingabe löschen"
edit: "Titel und Kategorie dieses Themas ändern"
expand: "Aufklappen"
not_implemented: "Entschuldige, diese Funktion wurde noch nicht implementiert!"
@@ -1673,8 +1674,6 @@ de:
date_time_picker:
from: Von
to: An
- errors:
- to_before_from: "Das Datum muss später als das von-Datum sein."
emoji_picker:
filter_placeholder: Emoji suchen
smileys_&_emotion: Smileys und Emotion
@@ -1883,7 +1882,7 @@ de:
membership_request_accepted: "Mitgliedschaft akzeptiert in '%{group_name}' "
membership_request_consolidated:
one: "%{count} offene Mitgliedschaftsanfrage für '%{group_name}'"
- other: "%{count} offene Mitgliedschaftanfragen für '%{group_name}'"
+ other: "%{count} offene Mitgliedschaftsanfragen für '%{group_name}'"
reaction: "%{username} %{description}"
reaction_2: "%{username}, %{username2} %{description}"
votes_released: "%{description} - abgeschlossen"
@@ -1966,6 +1965,7 @@ de:
or_search_google: "Oder versuche stattdessen mit Google zu suchen:"
search_google: "Versuche stattdessen mit Google zu suchen:"
search_google_button: "Google"
+ search_button: "Suche"
context:
user: "Beiträge von @%{username} durchsuchen"
category: "Kategorie #%{category} durchsuchen"
@@ -2864,6 +2864,7 @@ de:
list_filters:
all: "alle Themen"
none: "keine Unterkategorien"
+ colors_disabled: "Sie können keine Farben auswählen, da Sie keinen Kategoriestil haben."
flagging:
title: "Danke für deine Mithilfe!"
action: "Beitrag melden"
@@ -3305,6 +3306,7 @@ de:
two_hours: "2 Stunden"
tomorrow: "Bis morgen"
custom: "Benutzerdefiniert"
+ set_schedule: "Zeitplan für Benachrichtigungen festlegen"
admin_js:
type_to_filter: "zum Filtern hier eingeben…"
admin:
@@ -3332,6 +3334,10 @@ de:
installed_version: "Installiert"
latest_version: "Neueste"
problems_found: "Ein paar Ratschläge basierend auf deinen aktuellen Website-Einstellungen"
+ new_features:
+ title: "\U0001F381 Neue Funktionen"
+ dismiss: "Verwerfen"
+ learn_more: "Mehr erfahren"
last_checked: "Zuletzt geprüft"
refresh_problems: "Aktualisieren"
no_problems: "Es wurden keine Probleme gefunden."
@@ -3462,6 +3468,9 @@ de:
group_members: "Gruppenmitglieder"
delete: "Löschen"
delete_confirm: "Diese Gruppe löschen?"
+ delete_with_messages_confirm:
+ one: "Wenn Sie diese Gruppe löschen, wird %{count} Nachricht verwaist, Gruppenmitglieder haben keinen Zugriff mehr darauf. Bist du sicher?"
+ other: "Wenn Sie diese Gruppe löschen, werden %{count} Nachrichten verwaist, Gruppenmitglieder haben keinen Zugriff mehr auf sie. Bist du sicher?"
delete_failed: "Gruppe konnte nicht gelöscht werden. Wenn dies eine automatische Gruppe ist, kann sie nicht gelöscht werden."
delete_owner_confirm: "Eigentümerrechte für '%{username}' entfernen?"
add: "Hinzufügen"
@@ -3883,9 +3892,7 @@ de:
Beispiel:
- :root {
- --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)};
- }
+ %{example}
Eigenschaften sollten mit einem Präfix versehen werden, um Konflikte mit Discourse und Plugins zu vermeiden.
head_tag:
@@ -4382,6 +4389,9 @@ de:
anonymize_yes: "Ja, diesen Benutzer anonymisieren"
anonymize_failed: "Beim Anonymisieren des Benutzers ist ein Fehler aufgetreten."
delete: "Benutzer löschen"
+ delete_posts:
+ progress:
+ title: "Fortschritt beim Löschen von Beiträgen"
merge:
button: "Zusammenführen"
prompt:
@@ -4393,6 +4403,8 @@ de:
target_username_placeholder: "Benutzername des neuen Eigentümers"
transfer_and_delete: "Übertragen & Löschen @%{username}"
cancel: "Abbrechen"
+ progress:
+ title: "Fortschritt der Zusammenführung"
confirmation:
title: "Übertragen & Löschen @%{username}"
description: |
@@ -4540,6 +4552,7 @@ de:
recommended: "Wir empfehlen, dass du den folgenden Text an deine Bedürfnisse anpasst:"
show_overriden: "Nur geänderte Texte anzeigen"
locale: "Sprache:"
+ fallback_locale_warning: "Du bearbeitest eine Sprache basierend auf %{fallback}. Benutzer, die %{fallback} als Sprache ihrer Schnittstelle wählen, werden deine Änderungen nicht sehen."
more_than_50_results: "Es gibt mehr als 50 Ergebnisse. Bitte grenze deine Suche weiter ein."
settings:
show_overriden: "Nur geänderte anzeigen"
diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml
index 000fb2fe66..0e767ed114 100644
--- a/config/locales/client.el.yml
+++ b/config/locales/client.el.yml
@@ -1594,8 +1594,6 @@ el:
date_time_picker:
from: Από
to: Προς
- errors:
- to_before_from: "Η τελική ημερομηνία πρέπει να είναι αργότερα από την αρχική ημερομηνία."
emoji_picker:
filter_placeholder: Αναζήτηση για emoji
smileys_&_emotion: Χαμόγελα και συγκίνηση
diff --git a/config/locales/client.en_GB.yml b/config/locales/client.en_GB.yml
index 1c76c9c06f..d299959136 100644
--- a/config/locales/client.en_GB.yml
+++ b/config/locales/client.en_GB.yml
@@ -112,17 +112,6 @@ en_GB:
color_definitions:
text: "Colour Definitions"
title: "Enter custom colour definitions (advanced users only)"
- placeholder: |2-
-
- Use this stylesheet to add custom colours to the list of CSS custom properties.
-
- Example:
-
- :root {
- --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)};
- }
-
- Prefixing the property names is highly recommended to avoid conflicts with plugins and/or core.
colors:
select_base:
title: "Select base colour palette"
diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml
index 922da8a1e0..45878c36e1 100644
--- a/config/locales/client.es.yml
+++ b/config/locales/client.es.yml
@@ -424,6 +424,9 @@ es:
fields: "Campos"
reject_reason: "Motivo"
user_percentage:
+ summary:
+ one: "%{agreed}, %{disagreed}, %{ignored}, (del último reporte)"
+ other: "%{agreed}, %{disagreed}, %{ignored} (de los últimos %{count} reportes)"
agreed:
one: "%{count}% de acuerdo"
other: "%{count}% de acuerdo"
@@ -841,6 +844,20 @@ es:
mute_option_title: "No recibirás ninguna notificación relacionada con este usuario."
normal_option: "Normal"
normal_option_title: "Se te notificará si este usuario te responde, cita o menciona."
+ notification_schedule:
+ title: "Horario de notificaciones"
+ label: "Activar horario personalizado de notificaciones"
+ tip: "Fuera de estas horas, se activará el modo «no molestar» automáticamente."
+ midnight: "Medianoche"
+ none: "Ninguno"
+ monday: "Lunes"
+ tuesday: "Martes"
+ wednesday: "Miércoles"
+ thursday: "Jueves"
+ friday: "Viernes"
+ saturday: "Sábado"
+ sunday: "Domingo"
+ to: "a"
activity_stream: "Actividad"
preferences: "Preferencias"
feature_topic_on_profile:
@@ -870,6 +887,7 @@ es:
perm_denied_expl: "Has denegado el permiso para las notificaciones. Configura tu navegador para permitir notificaciones. "
disable: "Desactivar notificaciones"
enable: "Activar notificaciones"
+ each_browser_note: 'Nota: hay que cambiar este ajuste en cada navegador que uses. Todas las notificaciones serán desactivadas en el modo «no molestar», independientemente de este ajuste.'
consent_prompt: "¿Quieres recibir notificaciones en vivo cuando alguien responda a tus mensajes?"
dismiss: "Descartar"
dismiss_notifications: "Descartar todo"
@@ -1654,8 +1672,6 @@ es:
date_time_picker:
from: Desde
to: Hasta
- errors:
- to_before_from: "La fecha «hasta» debe ser posterior a la fecha «desde»."
emoji_picker:
filter_placeholder: Buscar emoji
smileys_&_emotion: Caras y emociones
@@ -1814,6 +1830,7 @@ es:
label: "Crear tema"
shared_draft:
label: "Borrador compartido"
+ desc: "Inicia un tema-borrador que solo será visible por los usuarios permitidos"
toggle_topic_bump:
label: "Alternar bump del tema"
desc: "Responder sin alterar la fecha de última respuesta"
@@ -1946,6 +1963,7 @@ es:
or_search_google: "O prueba buscar a través de Google:"
search_google: "Intenta buscar con Google:"
search_google_button: "Google"
+ search_button: "Buscar"
context:
user: "Buscar publicaciones de @%{username}"
category: "Buscar la categoría #%{category}"
@@ -2514,7 +2532,7 @@ es:
other: "ver %{count} publicaciones ocultas"
notice:
new_user: "Esta es la primera vez que %{user} ha publicado — ¡démosle la bienvenida a nuestra comunidad!"
- returning_user: "Hace tiempo que no vemos a %{user} — su última publicación fue %{time}. "
+ returning_user: "Hacía tiempo que no veíamos a %{user}, su última publicación fue %{time}."
unread: "Publicaciones sin leer"
has_replies:
one: "%{count} Respuesta"
@@ -2843,6 +2861,7 @@ es:
list_filters:
all: "todos los temas"
none: "sin subcategoría"
+ colors_disabled: "No puedes seleccionar colores porque tienes no tienes activados los estilos de categoría."
flagging:
title: "¡Gracias por ayudar a mantener una comunidad civilizada!"
action: "Reportar publicación"
@@ -2860,6 +2879,7 @@ es:
notify_action: "Mensaje"
official_warning: "Advertencia oficial"
delete_spammer: "Eliminar spammer"
+ delete_confirm_MF: "Estás a punto de eliminar {POSTS, plural, one {# publicación} other {# publicaciones}} y {TOPICS, plural, one {# tema} other {# temas}} de este usuario, también eliminarás su cuenta, bloquearás registros desde su dirección IP {ip_address} , y añadirás su correo electrónico {email} a una lista de bloqueos permanentes. ¿Seguro que el usuario es de verdad un spammer?"
yes_delete_spammer: "Sí, eliminar spammer"
ip_address_missing: "(N/D)"
hidden_email_address: "(oculto)"
@@ -2926,6 +2946,12 @@ es:
title: "Este tema es un mensaje personal"
help: "Este tema es un mensaje personal"
posts: "Publicaciones"
+ posts_likes_MF: |
+ Este tema tiene {count, plural, one {# respuesta} other {# respuestas}} {ratio, select,
+ low {con una proporción de me gusta por publicación alta}
+ med {con una proporción de me gusta por publicación muy alta}
+ high {con una proporción de me gusta por publicación extremadamente alta}
+ other {}}
original_post: "Publicación original"
views: "Vistas"
views_lowercase:
@@ -3255,6 +3281,7 @@ es:
invite:
custom_message: "Dale a tu invitación un toque personal escribiendo un mensaje personalizado ."
custom_message_placeholder: "Ingresa tu mensaje personalizado"
+ approval_not_required: "El usuario será aprobado automáticamente en cuanto acepte esta invitación."
custom_message_template_forum: "¡Hey, deberías unirte a este foro!"
custom_message_template_topic: "¡Hey, creemos que este tema te va a encantar!"
forced_anonymous: "Debido a una carga extrema, esto se está mostrando temporalmente a todos como lo vería un usuario que no haya iniciado sesión."
@@ -3276,6 +3303,7 @@ es:
two_hours: "2 horas"
tomorrow: "Hasta mañana"
custom: "Personalizado"
+ set_schedule: "Establecer un horario de notificaciones"
admin_js:
type_to_filter: "filtrar opciones..."
admin:
@@ -3303,6 +3331,10 @@ es:
installed_version: "Instalada"
latest_version: "Última"
problems_found: "Algunos consejos con base en tus ajustes actuales"
+ new_features:
+ title: "\U0001F381 Novedades"
+ dismiss: "Ocultar"
+ learn_more: "Más información"
last_checked: "Ultima comprobación"
refresh_problems: "Refrescar"
no_problems: "No se encontró ningún problema."
@@ -3793,6 +3825,7 @@ es:
install_upload: "Desde tu dispositivo"
install_git_repo: "Desde un repositorio git"
install_create: "Crear nuevo"
+ duplicate_remote_theme: "El componente de tema «%{name}» ya está instalado, ¿seguro que quieres instalar otra copia?"
about_theme: "Acerca de"
license: "Licencia"
version: "Versión:"
@@ -3812,6 +3845,7 @@ es:
check_for_updates: "Buscar actualizaciones"
updating: "Actualizando..."
up_to_date: "El tema está actualizado. Última comprobación:"
+ has_overwritten_history: "La versión actual del tema ya no existe, porque el historial de Git ha sido sobreescrito por un force push."
add: "Agregar"
theme_settings: "Ajustes del tema"
no_settings: "Este tema no tiene ajustes."
@@ -3841,17 +3875,6 @@ es:
color_definitions:
text: "Definiciones de color"
title: "Introducir definiciones de colores personalizadas (para usuarios avanzados)"
- placeholder: |2-
-
- Usa esta hoja de estilos para añadir colores personalizados a la lista de propiedades personalizadas de CSS.
-
- Por ejemplo:
-
- :root {
- --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)};
- }
-
- Añadir un prefijo a los nombres de las propiedades es altamente recomendado para evitar conflictos con plugins y el núcleo.
head_tag:
text: ""
title: "HTML que será insertado antes de la etiqueta"
@@ -4297,6 +4320,7 @@ es:
clear_penalty_history:
title: "Eliminado historial de faltas"
description: "usuarios con faltas no pueden alcanzar NC3"
+ delete_all_posts_confirm_MF: "Estás a punto de eliminar {POSTS, plural, one {# publicación} other {# publicaciones}} y {TOPICS, plural, one {# tema} other {# temas}}. ¿Estás seguro?"
silence: "Silenciar"
unsilence: "Dejar de silenciar"
silenced: "¿Silenciado?"
@@ -4345,6 +4369,9 @@ es:
anonymize_yes: "Sí, hacer anónima esta cuenta."
anonymize_failed: "Hubo un problema al hacer anónima la cuenta."
delete: "Eliminar usuario"
+ delete_posts:
+ progress:
+ title: "Progreso de la eliminación de publicaciones"
merge:
button: "Juntar"
prompt:
@@ -4356,6 +4383,8 @@ es:
target_username_placeholder: "Nombre de usuario del nuevo dueño"
transfer_and_delete: "Transferir y eliminar @%{username}"
cancel: "Cancelar"
+ progress:
+ title: "Progreso de fusión"
confirmation:
title: "Transferir y eliminar @%{username}"
description: |
@@ -4502,6 +4531,7 @@ es:
go_back: "Volver a la búsqueda"
recommended: "Recomendamos personalizar el siguiente texto para que se ajusten a tus necesidades:"
show_overriden: "Solo mostrar sobrescritos"
+ locale: "Idioma:"
more_than_50_results: "Hay más de 50 resultados. Por favor, afina tu busqueda."
settings:
show_overriden: "Solo mostrar anulados"
diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml
index f06d408af5..8ea48d0093 100644
--- a/config/locales/client.fi.yml
+++ b/config/locales/client.fi.yml
@@ -1603,8 +1603,6 @@ fi:
date_time_picker:
from: Mistä
to: Vastaanottaja
- errors:
- to_before_from: "Päättymispäivän on oltava myöhempi kuin alkamispäivän."
emoji_picker:
filter_placeholder: Etsi emojia
smileys_&_emotion: Hymiöt ja tunteet
diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml
index 8dada8822d..dfb9ba7e74 100644
--- a/config/locales/client.fr.yml
+++ b/config/locales/client.fr.yml
@@ -844,6 +844,19 @@ fr:
mute_option_title: "Vous ne recevrez pas de notifications en lien avec cet utilisateur."
normal_option: "Normal"
normal_option_title: "Vous serez notifié si cet utilisateur vous répond, vous cite ou vous mentionne."
+ notification_schedule:
+ title: "Planification des notifications"
+ label: "Activer la planification des notifications personnalisées"
+ tip: "En dehors de ces heures, vous serez automatiquement mis en \"ne pas déranger\"."
+ midnight: "Minuit"
+ none: "Aucun"
+ monday: "Lundi"
+ tuesday: "Mardi"
+ wednesday: "Mercredi"
+ thursday: "Jeudi"
+ friday: "Vendredi"
+ saturday: "samedi"
+ sunday: "dimanche"
activity_stream: "Activité"
preferences: "Préférences"
feature_topic_on_profile:
@@ -890,7 +903,7 @@ fr:
color_schemes:
default_description: "Couleur du thème"
disable_dark_scheme: "Désactivé"
- dark_instructions: "Vous pouvez prévisualiser le jeu de couleurs du mode sombre en basculant le mode sombre de votre appareil."
+ dark_instructions: "Vous pouvez prévisualiser le jeu de couleurs du mode sombre en activant le mode sombre de votre appareil."
undo: "Réinitialiser"
regular: "Normal"
dark: "Mode sombre"
@@ -1659,8 +1672,6 @@ fr:
date_time_picker:
from: De
to: À
- errors:
- to_before_from: "La date de fin doit être postérieure à celle de début."
emoji_picker:
filter_placeholder: Rechercher un émoji
smileys_&_emotion: Frimousses et émotions
@@ -3292,6 +3303,7 @@ fr:
two_hours: "2 heures"
tomorrow: "Jusqu'à demain"
custom: "Personnalisé"
+ set_schedule: "Planifier les notifications"
admin_js:
type_to_filter: "commencez à taper pour filtrer…"
admin:
@@ -3319,6 +3331,10 @@ fr:
installed_version: "Installée"
latest_version: "Dernière"
problems_found: "Quelques conseils d'après vos paramètres actuels"
+ new_features:
+ title: "\U0001F381 Nouvelles fonctionnalités"
+ dismiss: "Ignorer"
+ learn_more: "En savoir plus…"
last_checked: "Dernière vérification"
refresh_problems: "Actualiser"
no_problems: "Aucun problème n'a été trouvé."
@@ -3862,17 +3878,6 @@ fr:
color_definitions:
text: "Définitions de couleur"
title: "Entrez des définitions de couleurs personnalisées (utilisateurs avancés uniquement)"
- placeholder: |2-
-
- Utilisez cette feuille de style pour ajouter des couleurs personnalisées à la liste des propriétés personnalisées CSS.
-
- Exemple :
-
- :root {
- --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)};
- }
-
- Il est fortement recommandé de préfixer les noms de propriété pour éviter les conflits avec Discourse ou ses extensions.
head_tag:
text: ""
title: "HTML qui sera inséré avant la balise "
diff --git a/config/locales/client.gl.yml b/config/locales/client.gl.yml
index a4c650dfca..b6ba6056ea 100644
--- a/config/locales/client.gl.yml
+++ b/config/locales/client.gl.yml
@@ -187,6 +187,7 @@ gl:
us_gov_west_1: "AWS GovCloud (EUA-Oeste)"
us_west_1: "EUA Oeste (N. California)"
us_west_2: "EUA Oeste (Oregón)"
+ clear_input: "Despexar entrada"
edit: "editar o título e a categoría deste tema"
expand: "Expandir"
not_implemented: "Sentímolo pero esta funcionalidade non se implementou aínda."
@@ -422,6 +423,7 @@ gl:
email: "Correo electrónico"
name: "Nome"
fields: "Campos"
+ reject_reason: "Razón"
user_percentage:
summary:
one: "%{agreed}, %{disagreed}, %{ignored} (da última alerta)"
@@ -514,6 +516,9 @@ gl:
other: "Tes %{count} publicacións pendentes."
ok: "De acordo"
example_username: "nome de usuario"
+ reject_reason:
+ title: "Por que rexeita a este usuario?"
+ send_email: "Enviar correo de rexeitamento"
user_action:
user_posted_topic: "%{user} publicou o tema "
you_posted_topic: "Vostede publicou o tema "
@@ -840,6 +845,20 @@ gl:
mute_option_title: "Non recibirás notificación ningunha relacionada con este usuario."
normal_option: "Normal"
normal_option_title: "Recibirás unha notificación se este usuario che responde, cita a túa mensaxe ou menciona o teu nome."
+ notification_schedule:
+ title: "Calendario de notificacións"
+ label: "Activar o calendario personalizado de notificacións"
+ tip: "Fóra destas horas, vostede terá posto automaticamente en «non molestar»"
+ midnight: "Medianoite"
+ none: "Ningunha"
+ monday: "Luns"
+ tuesday: "Martes"
+ wednesday: "Mércores"
+ thursday: "Xoves"
+ friday: "Venres"
+ saturday: "Sábado"
+ sunday: "Domingo"
+ to: "para o"
activity_stream: "Actividade"
preferences: "Preferencias"
feature_topic_on_profile:
@@ -1655,8 +1674,6 @@ gl:
date_time_picker:
from: De
to: A
- errors:
- to_before_from: "A data límite debe ser posterior á de orixe."
emoji_picker:
filter_placeholder: Buscar emoji
smileys_&_emotion: Emoticonas e emoción
@@ -1948,6 +1965,7 @@ gl:
or_search_google: "Ou proba a buscar con Google:"
search_google: "Proba a buscar con Google:"
search_google_button: "Google"
+ search_button: "Buscar"
context:
user: "Buscar publicacións de @%{username}"
category: "Buscar na categoría #%{category}"
@@ -2846,6 +2864,7 @@ gl:
list_filters:
all: "todos os temas"
none: "sen subcategorías"
+ colors_disabled: "Non pode seleccionar cores porque ten un estilo de categoría con ningún."
flagging:
title: "Grazas por axudar a manter a nosa comunidade."
action: "Denunciar publicación"
@@ -3287,6 +3306,7 @@ gl:
two_hours: "2 horas"
tomorrow: "Ata mañá"
custom: "Personalizado"
+ set_schedule: "Estabelecer un calendario de notificacións"
admin_js:
type_to_filter: "escribe para filtrar..."
admin:
@@ -3314,6 +3334,10 @@ gl:
installed_version: "Instalado"
latest_version: "Últimos"
problems_found: "Algúns consellos baseados na súa configuración actual do sitio"
+ new_features:
+ title: "Novas funcionalidades"
+ dismiss: "Desbotar"
+ learn_more: "Saber máis"
last_checked: "Última comprobación"
refresh_problems: "Actualizar"
no_problems: "Non se atoparon problemas."
@@ -3444,6 +3468,9 @@ gl:
group_members: "Membros do grupo"
delete: "Eliminar"
delete_confirm: "Eliminar este grupo?"
+ delete_with_messages_confirm:
+ one: "Se elimina este grupo, quedará orfo %{count} mensaxe, os membros do grupo xa non terán acceso a el. Está seguro?"
+ other: "Se elimina este grupo, quedará orfo %{count} mensaxes, os membros do grupo xa non terán acceso a el. Está seguro?"
delete_failed: "Non é posíbel eliminar o grupo. Se este é un grupo automático, non se pode destruír."
delete_owner_confirm: "Eliminar os privilexios de usuario de «%{username}»?"
add: "Engadir"
@@ -3861,15 +3888,13 @@ gl:
title: "Escriba definicións de cor personalizadas (só para usuarios avanzados)"
placeholder: |2-
- Utilice esta folla de estilo para engadir cores personalizadas á listaxe de propiedades CSS.
+ Utilice esta folla de estilo para engadir cores personalizadas á lista de propiedades de CSS personalizadas
Exemplo:
- :root {
- --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)};
- }
+ %{example}
- Prefixar os nomes da propiedade resulta altamente recomendábel para evitarmos conflitos con complementos e/ou o núcleo.
+ Recoméndase intensamente prefixar os nomes para evitar conflitos con complementos e/ou o núcleo.
head_tag:
text: ""
title: "HTML que se inserirá antes da etiqueta "
@@ -4364,6 +4389,9 @@ gl:
anonymize_yes: "Si, converter a conta en anónima"
anonymize_failed: "Produciuse un problema convertendo a conta en anónima."
delete: "Eliminar usuario"
+ delete_posts:
+ progress:
+ title: "Progreso na eliminación de publicacións"
merge:
button: "Combinar"
prompt:
@@ -4375,6 +4403,8 @@ gl:
target_username_placeholder: "Nome de usuario do novo propietario"
transfer_and_delete: "Transferir e eliminar @%{username}"
cancel: "Cancelar"
+ progress:
+ title: "Combinar o progreso"
confirmation:
title: "Transferir e eliminar @%{username}"
description: |
@@ -4521,6 +4551,8 @@ gl:
go_back: "Volver á busca"
recommended: "Recomendamos personalizar o seguinte texto para axeitalo ás túas necesidades:"
show_overriden: "Amosar só os cambios"
+ locale: "Idioma:"
+ fallback_locale_warning: "Está a editar un idioma baseado en %{fallback}. Os usuarios que escollan %{fallback} como idioma de interface non verán os cambios."
more_than_50_results: "Hai máis de 50 resultados. Restrinxe a busca."
settings:
show_overriden: "Amosar só os cambios"
diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml
index 7466ef4396..c4242b623f 100644
--- a/config/locales/client.he.yml
+++ b/config/locales/client.he.yml
@@ -233,6 +233,7 @@ he:
us_gov_west_1: "הענן הממשלתי של AWS (מערב ארה״ב)"
us_west_1: "מערב ארה״ב (צפון קליפורניה)"
us_west_2: "מערב ארה״ב (אורגון)"
+ clear_input: "פינוי הקלט"
edit: "עריכת הכותרת והקטגוריה של נושא זה"
expand: "הרחב"
not_implemented: "תכונה זו עדיין לא מומשה, עמך הסליחה!"
@@ -1788,8 +1789,6 @@ he:
date_time_picker:
from: מאת
to: אל
- errors:
- to_before_from: "תאריך היעד חייב להיות לאחר תאריך ההתחלה."
emoji_picker:
filter_placeholder: חיפוש אחר אמוג׳י
smileys_&_emotion: חייכנים ורגש
@@ -3599,6 +3598,10 @@ he:
installed_version: "הותקן"
latest_version: "אחרונה"
problems_found: "עצה שמבוססת על הגדרות האתר הנוכחיות שלך"
+ new_features:
+ title: "\U0001F381 תכונות חדשות"
+ dismiss: "התעלמות"
+ learn_more: "מידע נוסף"
last_checked: "נבדק לאחרונה"
refresh_problems: "רענן"
no_problems: "לא נמצאו בעיות."
@@ -3731,6 +3734,11 @@ he:
group_members: "חברי הקבוצה"
delete: "מחיקה"
delete_confirm: "להסיר קבוצה זו?"
+ delete_with_messages_confirm:
+ one: "מחיקת הקבוצה הזאת תוביל להתייתמותה של הודעה %{count}, חברי הקבוצה לא יוכלו לגשת אליה עוד. להמשיך?"
+ two: "מחיקת הקבוצה הזאת תוביל להתייתמותן של %{count} הודעות, חברי הקבוצה לא יוכלו לגשת אליהן עוד. להמשיך?"
+ many: "מחיקת הקבוצה הזאת תוביל להתייתמותן של %{count} הודעות, חברי הקבוצה לא יוכלו לגשת אליהן עוד. להמשיך?"
+ other: "מחיקת הקבוצה הזאת תוביל להתייתמותן של %{count} הודעות, חברי הקבוצה לא יוכלו לגשת אליהן עוד. להמשיך?"
delete_failed: "לא ניתן להסיר קבוצה זו. אם זו קבוצה אוטומטית, היא בלתי ניתנת למחיקה."
delete_owner_confirm: "להסיר את הרשאות הניהול של ‚%{username}’?"
add: "הוספה"
@@ -4158,17 +4166,15 @@ he:
דוגמה:
- :root {
- --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)};
- }
+ %{example}
מומלץ מאוד להוסיף קידומת לשמות המאפיינים כדי להימנע מסתירות מול תוספים ו/או הליבה.
head_tag:
- text: ""
- title: "HTML שיוכנס לפני התג "
+ text: "head>"
+ title: "HTML שיוכנס לפני התג head>"
body_tag:
- text: "
-
The change you wanted was rejected.
+
The change you wanted has been rejected.
Maybe you tried to change something you didn't have access to.