FEATURE: Option to update category preferences of all users when site setting changed (#8180)

FEATURE: Option to update category preferences of all users when site setting changed (#8180)

diff --git a/app/assets/javascripts/admin/components/site-setting.js.es6 b/app/assets/javascripts/admin/components/site-setting.js.es6
index 78696c6e06..4c1d76cad8 100644
--- a/app/assets/javascripts/admin/components/site-setting.js.es6
+++ b/app/assets/javascripts/admin/components/site-setting.js.es6
@@ -1,10 +1,54 @@
 import BufferedContent from "discourse/mixins/buffered-content";
 import SiteSetting from "admin/models/site-setting";
 import SettingComponent from "admin/mixins/setting-component";
+import showModal from "discourse/lib/show-modal";
+import AboutRoute from "discourse/routes/about";
 
 export default Ember.Component.extend(BufferedContent, SettingComponent, {
-  _save() {
+  update(key, value, updateExistingUsers = false) {
+    if (updateExistingUsers) {
+      return SiteSetting.update(key, value, { updateExistingUsers: true });
+    } else {
+      return SiteSetting.update(key, value);
+    }
+  },
+
+  _save(callback) {
+    const defaultCategoriesSettings = [
+      "default_categories_watching",
+      "default_categories_tracking",
+      "default_categories_muted",
+      "default_categories_watching_first_post"
+    ];
     const setting = this.buffered;
-    return SiteSetting.update(setting.get("setting"), setting.get("value"));
+    const key = setting.get("setting");
+    const value = setting.get("value");
+
+    if (defaultCategoriesSettings.includes(key)) {
+      AboutRoute.create()
+        .model()
+        .then(result => {
+          const controller = showModal("site-setting-default-categories", {
+            model: {
+              count: result.stats.user_count,
+              key: key.replace(/_/g, " ")
+            },
+            admin: true
+          });
+
+          controller.setProperties({
+            onClose: () => {
+              const updateExistingUsers = controller.get("updateExistingUsers");
+              if (updateExistingUsers === true) {
+                callback(this.update(key, value, true));
+              } else if (updateExistingUsers === false) {
+                callback(this.update(key, value));
+              }
+            }
+          });
+        });
+    } else {
+      callback(this.update(key, value));
+    }
   }
 });
diff --git a/app/assets/javascripts/admin/controllers/modals/site-setting-default-categories.js.es6 b/app/assets/javascripts/admin/controllers/modals/site-setting-default-categories.js.es6
new file mode 100644
index 0000000000..5b856f1dbc
--- /dev/null
+++ b/app/assets/javascripts/admin/controllers/modals/site-setting-default-categories.js.es6
@@ -0,0 +1,19 @@
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+
+export default Ember.Controller.extend(ModalFunctionality, {
+  onShow() {
+    this.set("updateExistingUsers", null);
+  },
+
+  actions: {
+    updateExistingUsers() {
+      this.set("updateExistingUsers", true);
+      this.send("closeModal");
+    },
+
+    cancel() {
+      this.set("updateExistingUsers", false);
+      this.send("closeModal");
+    }
+  }
+});
diff --git a/app/assets/javascripts/admin/mixins/setting-component.js.es6 b/app/assets/javascripts/admin/mixins/setting-component.js.es6
index 18be48a2d1..a2d5b3a4de 100644
--- a/app/assets/javascripts/admin/mixins/setting-component.js.es6
+++ b/app/assets/javascripts/admin/mixins/setting-component.js.es6
@@ -111,21 +111,23 @@ export default Ember.Mixin.create({
 
   actions: {
     save() {
-      this._save()
-        .then(() => {
-          this.set("validationMessage", null);
-          this.commitBuffer();
-          if (AUTO_REFRESH_ON_SAVE.includes(this.get("setting.setting"))) {
-            this.afterSave();
-          }
-        })
-        .catch(e => {
-          if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
-            this.set("validationMessage", e.jqXHR.responseJSON.errors[0]);
-          } else {
-            this.set("validationMessage", I18n.t("generic_error"));
-          }
-        });
+      this._save(result => {
+        result
+          .then(() => {
+            this.set("validationMessage", null);
+            this.commitBuffer();
+            if (AUTO_REFRESH_ON_SAVE.includes(this.get("setting.setting"))) {
+              this.afterSave();
+            }
+          })
+          .catch(e => {
+            if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
+              this.set("validationMessage", e.jqXHR.responseJSON.errors[0]);
+            } else {
+              this.set("validationMessage", I18n.t("generic_error"));
+            }
+          });
+      });
     },
 
     cancel() {
diff --git a/app/assets/javascripts/admin/models/site-setting.js.es6 b/app/assets/javascripts/admin/models/site-setting.js.es6
index ca1c175864..7760a61114 100644
--- a/app/assets/javascripts/admin/models/site-setting.js.es6
+++ b/app/assets/javascripts/admin/models/site-setting.js.es6
@@ -25,9 +25,14 @@ SiteSetting.reopenClass({
     });
   },
 
-  update(key, value) {
+  update(key, value, opts = {}) {
     const data = {};
     data[key] = value;
+
+    if (opts["updateExistingUsers"] === true) {
+      data["updateExistingUsers"] = true;
+    }
+
     return ajax(`/admin/site_settings/${key}`, { type: "PUT", data });
   }
 });
diff --git a/app/assets/javascripts/admin/templates/modal/site-setting-default-categories.hbs b/app/assets/javascripts/admin/templates/modal/site-setting-default-categories.hbs
new file mode 100644
index 0000000000..e56bf01c65
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/modal/site-setting-default-categories.hbs
@@ -0,0 +1,8 @@
+{{#d-modal-body class="incoming-emails" rawTitle=model.key}}
+{{i18n "admin.site_settings.default_categories.modal_description" count=model.count}}
+{{/d-modal-body}}
+
+<div class="modal-footer">
+  {{d-button action=(action "updateExistingUsers") class='btn btn-primary' label='admin.site_settings.default_categories.modal_yes'}}
+  {{d-button action=(action "cancel") class='btn' label='admin.site_settings.default_categories.modal_no'}}
+</div>
diff --git a/app/controllers/admin/site_settings_controller.rb b/app/controllers/admin/site_settings_controller.rb
index 546d7974f1..7a066924c3 100644
--- a/app/controllers/admin/site_settings_controller.rb
+++ b/app/controllers/admin/site_settings_controller.rb
@@ -20,7 +20,42 @@ class Admin::SiteSettingsController < Admin::AdminController
       value = Upload.find_by(url: value) || ''
     end
 
+    update_existing_users = params[:updateExistingUsers].present?
+    previous_category_ids = (SiteSetting.send(id) || "").split("|") if update_existing_users
+
     SiteSetting.set_and_log(id, value, current_user)
+
+    if update_existing_users
+      new_category_ids = (value || "").split("|")
+
+      case id
+      when "default_categories_watching"
+        notification_level = NotificationLevels.all[:watching]
+      when "default_categories_tracking"
+        notification_level = NotificationLevels.all[:tracking]
+      when "default_categories_muted"
+        notification_level = NotificationLevels.all[:muted]
+      when "default_categories_watching_first_post"
+        notification_level = NotificationLevels.all[:watching_first_post]
+      end
+
+      (previous_category_ids - new_category_ids).each do |category_id|
+        CategoryUser.where(category_id: category_id, notification_level: notification_level).delete_all
+      end
+
+      (new_category_ids - previous_category_ids).each do |category_id|
+        skip_user_ids = CategoryUser.where(category_id: category_id).pluck(:user_id)
+
+        User.where.not(id: skip_user_ids).select(:id).find_in_batches do |users|
+          category_users = []
+          users.each { |user| category_users << { category_id: category_id, user_id: user.id, notification_level: notification_level } }
+          CategoryUser.insert_all!(category_users)
+        end
+
+        CategoryUser.where(category_id: category_id, notification_level: notification_level).first_or_create!(notification_level: notification_level)
+      end
+    end
+
     render body: nil
   end
 

[... diff too long, it was truncated ...]

GitHub sha: b2f682f3

This commit has been mentioned on Discourse Meta. There might be relevant details there:

any reason no doing setting.setting here?

any reason no doing setting.value here?

any reason no doing controller.updateExistingUsers here?

why setProperties ? I see only one property being set here

any reason not using this.setting.setting ?

You don’t need “btn” class, it’s already in d-button

You don’t need “btn” class, it’s already in d-button

I think this should follow the same pattern you use for the other method and have this method signature:

update(key, value, opts = {})

Then you can refactor with something like this:

return SiteSetting.update(key, value, Ember.getProperties(opts, "updateExistingUsers"));

Or even simper if you are sure you want to pass all options:

return SiteSetting.update(key, value, opts);

I didn’t write most of these codes. I wrapped the existing code in callback block. So I didn’t notice it :man_facepalming:

REFACTOR: improve the code readability. (#8211)

It looks like setting is a proxy object. So the code setting.setting is not working there.

same as FEATURE: Option to update category preferences of all users when site… · discourse/discourse@b2f682f · GitHub