diff --git a/app/assets/javascripts/discourse/app/components/table-header-toggle.js b/app/assets/javascripts/discourse/app/components/table-header-toggle.js index afe7ef32eb3..424879f546b 100644 --- a/app/assets/javascripts/discourse/app/components/table-header-toggle.js +++ b/app/assets/javascripts/discourse/app/components/table-header-toggle.js @@ -9,6 +9,7 @@ export default Component.extend({ chevronIcon: null, columnIcon: null, translated: false, + automatic: false, onActiveRender: null, toggleProperties() { @@ -31,6 +32,9 @@ export default Component.extend({ }, didReceiveAttrs() { this._super(...arguments); + if (!this.automatic && !this.translated) { + this.set("labelKey", this.field); + } this.set("id", `table-header-toggle-${this.field.replace(/\s/g, "")}`); this.toggleChevron(); }, diff --git a/app/assets/javascripts/discourse/app/controllers/edit-user-directory-columns.js b/app/assets/javascripts/discourse/app/controllers/edit-user-directory-columns.js index a7fd826af8b..771d5b9b306 100644 --- a/app/assets/javascripts/discourse/app/controllers/edit-user-directory-columns.js +++ b/app/assets/javascripts/discourse/app/controllers/edit-user-directory-columns.js @@ -14,7 +14,7 @@ export default Controller.extend(ModalFunctionality, { labelKey: null, onShow() { - ajax("directory-columns.json") + ajax("edit-directory-columns.json") .then((response) => { this.setProperties({ loading: false, @@ -35,7 +35,7 @@ export default Controller.extend(ModalFunctionality, { ), }; - ajax("directory-columns.json", { type: "PUT", data }) + ajax("edit-directory-columns.json", { type: "PUT", data }) .then(() => { reload(); }) @@ -58,7 +58,7 @@ export default Controller.extend(ModalFunctionality, { .forEach((column, index) => { column.setProperties({ position: column.automatic_position || index + 1, - enabled: column.automatic, + enabled: column.type === "automatic", }); }); this.set("columns", resetColumns); diff --git a/app/assets/javascripts/discourse/app/controllers/users.js b/app/assets/javascripts/discourse/app/controllers/users.js index e2878823ae4..1846b4eea3f 100644 --- a/app/assets/javascripts/discourse/app/controllers/users.js +++ b/app/assets/javascripts/discourse/app/controllers/users.js @@ -28,13 +28,22 @@ export default Controller.extend({ this.set("nameInput", params.name); this.set("order", params.order); - const custom_field_columns = this.columns.filter((c) => !c.automatic); - const user_field_ids = custom_field_columns - .map((c) => c.user_field_id) - .join("|"); + const userFieldColumns = this.columns.filter( + (c) => c.type === "user_field" + ); + const userFieldIds = userFieldColumns.map((c) => c.user_field_id).join("|"); + + const pluginColumns = this.columns.filter((c) => c.type === "plugin"); + const pluginColumnIds = pluginColumns.map((c) => c.id).join("|"); return this.store - .find("directoryItem", Object.assign(params, { user_field_ids })) + .find( + "directoryItem", + Object.assign(params, { + user_field_ids: userFieldIds, + plugin_column_ids: pluginColumnIds, + }) + ) .then((model) => { const lastUpdatedAt = model.get("resultSetMeta.last_updated_at"); this.setProperties({ diff --git a/app/assets/javascripts/discourse/app/helpers/directory-item-helpers.js b/app/assets/javascripts/discourse/app/helpers/directory-item-helpers.js new file mode 100644 index 00000000000..1007a506b7f --- /dev/null +++ b/app/assets/javascripts/discourse/app/helpers/directory-item-helpers.js @@ -0,0 +1,37 @@ +import { htmlSafe } from "@ember/template"; +import { number } from "discourse/lib/formatter"; +import { registerUnbound } from "discourse-common/lib/helpers"; +import I18n from "I18n"; + +registerUnbound("mobile-directory-item-label", function (args) { + // Args should include key/values { item, column } + const count = args.item.get(args.column.name); + return htmlSafe(I18n.t(`directory.${args.column.name}`, { count })); +}); + +registerUnbound("directory-item-value", function (args) { + // Args should include key/values { item, column } + return htmlSafe( + `${number(args.item.get(args.column.name))}` + ); +}); + +registerUnbound("directory-item-user-field-value", function (args) { + // Args should include key/values { item, column } + const value = + args.item.user && args.item.user.user_fields + ? args.item.user.user_fields[args.column.user_field_id] + : null; + const content = value || "-"; + return htmlSafe(`${content}`); +}); + +registerUnbound("directory-column-is-automatic", function (args) { + // Args should include key/values { column } + return args.column.type === "automatic"; +}); + +registerUnbound("directory-column-is-user-field", function (args) { + // Args should include key/values { column } + return args.column.type === "user_field"; +}); diff --git a/app/assets/javascripts/discourse/app/helpers/directory-item-label.js b/app/assets/javascripts/discourse/app/helpers/directory-item-label.js deleted file mode 100644 index 56723ee716e..00000000000 --- a/app/assets/javascripts/discourse/app/helpers/directory-item-label.js +++ /dev/null @@ -1,10 +0,0 @@ -import { htmlSafe } from "@ember/template"; -import { registerUnbound } from "discourse-common/lib/helpers"; -import I18n from "I18n"; - -export default registerUnbound("mobile-directory-item-label", function (args) { - // Args should include key/values { item, column } - - const count = args.item.get(args.column.name); - return htmlSafe(I18n.t(`directory.${args.column.name}`, { count })); -}); diff --git a/app/assets/javascripts/discourse/app/helpers/directory-item-user-field-value.js b/app/assets/javascripts/discourse/app/helpers/directory-item-user-field-value.js deleted file mode 100644 index aeab4bcbe12..00000000000 --- a/app/assets/javascripts/discourse/app/helpers/directory-item-user-field-value.js +++ /dev/null @@ -1,16 +0,0 @@ -import { htmlSafe } from "@ember/template"; -import { registerUnbound } from "discourse-common/lib/helpers"; - -export default registerUnbound( - "directory-item-user-field-value", - function (args) { - // Args should include key/values { item, column } - - const value = - args.item.user && args.item.user.user_fields - ? args.item.user.user_fields[args.column.user_field_id] - : null; - const content = value || "-"; - return htmlSafe(`${content}`); - } -); diff --git a/app/assets/javascripts/discourse/app/helpers/directory-item-value.js b/app/assets/javascripts/discourse/app/helpers/directory-item-value.js deleted file mode 100644 index a3c6e3d6d38..00000000000 --- a/app/assets/javascripts/discourse/app/helpers/directory-item-value.js +++ /dev/null @@ -1,11 +0,0 @@ -import { htmlSafe } from "@ember/template"; -import { registerUnbound } from "discourse-common/lib/helpers"; -import { number } from "discourse/lib/formatter"; - -export default registerUnbound("directory-item-value", function (args) { - // Args should include key/values { item, column } - - return htmlSafe( - `${number(args.item.get(args.column.name))}` - ); -}); diff --git a/app/assets/javascripts/discourse/app/routes/users.js b/app/assets/javascripts/discourse/app/routes/users.js index 181204d7864..ed1ac2cf84b 100644 --- a/app/assets/javascripts/discourse/app/routes/users.js +++ b/app/assets/javascripts/discourse/app/routes/users.js @@ -1,6 +1,7 @@ import DiscourseRoute from "discourse/routes/discourse"; import I18n from "I18n"; -import PreloadStore from "discourse/lib/preload-store"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; import { Promise } from "rsvp"; export default DiscourseRoute.extend({ @@ -38,9 +39,12 @@ export default DiscourseRoute.extend({ }, model(params) { - const columns = PreloadStore.get("directoryColumns"); - params.order = params.order || columns[0].name; - return { params, columns }; + return ajax("directory-columns.json") + .then((response) => { + params.order = params.order || response.directory_columns[0].name; + return { params, columns: response.directory_columns }; + }) + .catch(popupAjaxError); }, setupController(controller, model) { diff --git a/app/assets/javascripts/discourse/app/templates/components/directory-item.hbs b/app/assets/javascripts/discourse/app/templates/components/directory-item.hbs index 0d4fece4112..b1b083beda4 100644 --- a/app/assets/javascripts/discourse/app/templates/components/directory-item.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/directory-item.hbs @@ -1,10 +1,10 @@ {{user-info user=item.user}} {{#each columns as |column|}} - {{#if column.automatic}} - {{directory-item-value item=item column=column}} - {{else}} + {{#if (directory-column-is-user-field column=column)}} {{directory-item-user-field-value item=item column=column}} + {{else}} + {{directory-item-value item=item column=column}} {{/if}} {{/each}} diff --git a/app/assets/javascripts/discourse/app/templates/components/directory-table.hbs b/app/assets/javascripts/discourse/app/templates/components/directory-table.hbs index 1aafa640cc0..a646d794db6 100644 --- a/app/assets/javascripts/discourse/app/templates/components/directory-table.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/directory-table.hbs @@ -7,6 +7,7 @@ icon=column.icon order=order asc=asc + automatic=(directory-column-is-automatic column=column) translated=column.user_field_id onActiveRender=setActiveHeader }} diff --git a/app/assets/javascripts/discourse/app/templates/group-index.hbs b/app/assets/javascripts/discourse/app/templates/group-index.hbs index d5a7770478b..abe17a0f2c4 100644 --- a/app/assets/javascripts/discourse/app/templates/group-index.hbs +++ b/app/assets/javascripts/discourse/app/templates/group-index.hbs @@ -35,11 +35,11 @@ {{d-button action=(action "bulkClearAll") label="topics.bulk.clear_all"}} {{/if}} - {{table-header-toggle order=order asc=asc field="username_lower" labelKey="username" class="username"}} + {{table-header-toggle order=order asc=asc field="username_lower" labelKey="username" class="username" automatic=true}} - {{table-header-toggle order=order asc=asc field="added_at" labelKey="groups.member_added"}} - {{table-header-toggle order=order asc=asc field="last_posted_at" labelKey="last_post"}} - {{table-header-toggle order=order asc=asc field="last_seen_at" labelKey="last_seen"}} + {{table-header-toggle order=order asc=asc field="added_at" labelKey="groups.member_added" automatic=true}} + {{table-header-toggle order=order asc=asc field="last_posted_at" labelKey="last_post" automatic=true}} + {{table-header-toggle order=order asc=asc field="last_seen_at" labelKey="last_seen" automatic=true}} {{#if isBulk}} {{group-member-dropdown diff --git a/app/assets/javascripts/discourse/app/templates/group-requests.hbs b/app/assets/javascripts/discourse/app/templates/group-requests.hbs index 017aaaef083..b2f8d03ddfb 100644 --- a/app/assets/javascripts/discourse/app/templates/group-requests.hbs +++ b/app/assets/javascripts/discourse/app/templates/group-requests.hbs @@ -12,8 +12,8 @@ {{#load-more selector=".group-members tr" action=(action "loadMore")}} - {{table-header-toggle order=order asc=asc field="username_lower" labelKey="username"}} - {{table-header-toggle order=order asc=asc field="requested_at" labelKey="groups.member_requested"}} + {{table-header-toggle order=order asc=asc field="username_lower" labelKey="username" automatic=true}} + {{table-header-toggle order=order asc=asc field="requested_at" labelKey="groups.member_requested" automatic=true}} diff --git a/app/assets/javascripts/discourse/app/templates/mobile/components/directory-item.hbs b/app/assets/javascripts/discourse/app/templates/mobile/components/directory-item.hbs index e43ad11e263..3f75ed72788 100644 --- a/app/assets/javascripts/discourse/app/templates/mobile/components/directory-item.hbs +++ b/app/assets/javascripts/discourse/app/templates/mobile/components/directory-item.hbs @@ -1,7 +1,7 @@ {{user-info user=item.user}} {{#each columns as |column|}} - {{#if column.automatic}} + {{#if (directory-column-is-automatic column=column)}}
{{directory-item-value item=item column=column}} diff --git a/app/assets/javascripts/discourse/app/templates/modal/edit-user-directory-columns.hbs b/app/assets/javascripts/discourse/app/templates/modal/edit-user-directory-columns.hbs index fb3e86465eb..7fe81183a0b 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/edit-user-directory-columns.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/edit-user-directory-columns.hbs @@ -8,10 +8,12 @@
diff --git a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js index b312f02e25a..78f3c8cd90b 100644 --- a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js +++ b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js @@ -938,13 +938,13 @@ export function applyDefaultHandlers(pretender) { return [404, { "Content-Type": "application/html" }, ""]; }); - pretender.get("directory-columns.json", () => { + pretender.get("edit-directory-columns.json", () => { return response(200, { directory_columns: [ { id: 1, name: "likes_received", - automatic: true, + type: "automatic", enabled: true, automatic_position: 1, position: 1, @@ -954,7 +954,7 @@ export function applyDefaultHandlers(pretender) { { id: 2, name: "likes_given", - automatic: true, + type: "automatic", enabled: true, automatic_position: 2, position: 2, @@ -964,7 +964,7 @@ export function applyDefaultHandlers(pretender) { { id: 3, name: "topic_count", - automatic: true, + type: "automatic", enabled: true, automatic_position: 3, position: 3, @@ -974,7 +974,7 @@ export function applyDefaultHandlers(pretender) { { id: 4, name: "post_count", - automatic: true, + type: "automatic", enabled: true, automatic_position: 4, position: 4, @@ -984,7 +984,7 @@ export function applyDefaultHandlers(pretender) { { id: 5, name: "topics_entered", - automatic: true, + type: "automatic", enabled: true, automatic_position: 5, position: 5, @@ -994,7 +994,7 @@ export function applyDefaultHandlers(pretender) { { id: 6, name: "posts_read", - automatic: true, + type: "automatic", enabled: true, automatic_position: 6, position: 6, @@ -1004,7 +1004,7 @@ export function applyDefaultHandlers(pretender) { { id: 7, name: "days_visited", - automatic: true, + type: "automatic", enabled: true, automatic_position: 7, position: 7, @@ -1014,7 +1014,7 @@ export function applyDefaultHandlers(pretender) { { id: 9, name: null, - automatic: false, + type: "user_field", enabled: false, automatic_position: null, position: 8, @@ -1035,4 +1035,75 @@ export function applyDefaultHandlers(pretender) { ], }); }); + + pretender.get("directory-columns.json", () => { + return response(200, { + directory_columns: [ + { + id: 1, + name: "likes_received", + type: "automatic", + position: 1, + icon: "heart", + user_field: null, + }, + { + id: 2, + name: "likes_given", + type: "automatic", + position: 2, + icon: "heart", + user_field: null, + }, + { + id: 3, + name: "topic_count", + type: "automatic", + position: 3, + icon: null, + user_field: null, + }, + { + id: 4, + name: "post_count", + type: "automatic", + position: 4, + icon: null, + user_field: null, + }, + { + id: 5, + name: "topics_entered", + type: "automatic", + position: 5, + icon: null, + user_field: null, + }, + { + id: 6, + name: "posts_read", + type: "automatic", + position: 6, + icon: null, + user_field: null, + }, + { + id: 7, + name: "days_visited", + type: "automatic", + position: 7, + icon: null, + user_field: null, + }, + { + id: 9, + name: "Favorite Color", + type: "user_field", + position: 8, + icon: null, + user_field_id: 3, + }, + ], + }); + }); } diff --git a/app/assets/javascripts/discourse/tests/setup-tests.js b/app/assets/javascripts/discourse/tests/setup-tests.js index f51ff1cfdae..12f93098ab1 100644 --- a/app/assets/javascripts/discourse/tests/setup-tests.js +++ b/app/assets/javascripts/discourse/tests/setup-tests.js @@ -229,12 +229,6 @@ function setupTestsCommon(application, container, config) { }); PreloadStore.reset(); - PreloadStore.store( - "directoryColumns", - JSON.parse( - '[{"name":"likes_given","automatic":true,"icon":"heart","user_field_id":null},{"name":"posts_read","automatic":true,"icon":null,"user_field_id":null},{"name":"likes_received","automatic":true,"icon":"heart","user_field_id":null},{"name":"topic_count","automatic":true,"icon":null,"user_field_id":null},{"name":"post_count","automatic":true,"icon":null,"user_field_id":null},{"name":"topics_entered","automatic":true,"icon":null,"user_field_id":null},{"name":"days_visited","automatic":true,"icon":null,"user_field_id":null},{"name":"Favorite Color","automatic":false,"icon":null,"user_field_id":3}]' - ) - ); sinon.stub(ScrollingDOMMethods, "screenNotFull"); sinon.stub(ScrollingDOMMethods, "bindOnScroll"); diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ca9796b9621..acfa7e10802 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -604,7 +604,6 @@ class ApplicationController < ActionController::Base store_preloaded("customEmoji", custom_emoji) store_preloaded("isReadOnly", @readonly_mode.to_s) store_preloaded("activatedThemes", activated_themes_json) - store_preloaded("directoryColumns", directory_columns_json) end def preload_current_user_data @@ -616,20 +615,6 @@ class ApplicationController < ActionController::Base store_preloaded("topicTrackingStates", MultiJson.dump(serializer)) end - def directory_columns_json - DirectoryColumn - .left_joins(:user_field) - .where(enabled: true) - .order(:position) - .pluck('directory_columns.name', - 'directory_columns.automatic', - 'directory_columns.icon', - 'user_fields.id', - 'user_fields.name') - .map { |column| { name: column[0] || column[4], automatic: column[1], icon: column[2], user_field_id: column[3] } } - .to_json - end - def custom_html_json target = view_context.mobile_view? ? :mobile : :desktop diff --git a/app/controllers/directory_columns_controller.rb b/app/controllers/directory_columns_controller.rb index 2efdcd6dd4f..d11f5e30ff5 100644 --- a/app/controllers/directory_columns_controller.rb +++ b/app/controllers/directory_columns_controller.rb @@ -1,62 +1,8 @@ # frozen_string_literal: true class DirectoryColumnsController < ApplicationController - requires_login - def index - raise Discourse::NotFound unless guardian.is_staff? - - ensure_user_fields_have_columns - - columns = DirectoryColumn.includes(:user_field).all - render_json_dump(directory_columns: serialize_data(columns, DirectoryColumnSerializer)) - end - - def update - raise Discourse::NotFound unless guardian.is_staff? - params.require(:directory_columns) - directory_column_params = params.permit(directory_columns: {}) - directory_columns = DirectoryColumn.all - - has_enabled_column = directory_column_params[:directory_columns].values.any? do |column_data| - column_data[:enabled].to_s == "true" - end - raise Discourse::InvalidParameters, "Must have at least one column enabled" unless has_enabled_column - - directory_column_params[:directory_columns].values.each do |column_data| - existing_column = directory_columns.detect { |c| c.id == column_data[:id].to_i } - if (existing_column.enabled != column_data[:enabled] || existing_column.position != column_data[:position].to_i) - existing_column.update(enabled: column_data[:enabled], position: column_data[:position]) - end - end - - render json: success_json - end - - private - - def ensure_user_fields_have_columns - user_fields_without_column = - UserField.left_outer_joins(:directory_column) - .where(directory_column: { user_field_id: nil }) - .where("show_on_profile=? OR show_on_user_card=?", true, true) - - return unless user_fields_without_column.count > 0 - - next_position = DirectoryColumn.maximum("position") + 1 - - new_directory_column_attrs = [] - user_fields_without_column.each do |user_field| - new_directory_column_attrs.push({ - user_field_id: user_field.id, - enabled: false, - automatic: false, - position: next_position - }) - - next_position += 1 - end - - DirectoryColumn.insert_all(new_directory_column_attrs) + directory_columns = DirectoryColumn.includes(:user_field).where(enabled: true).order(:position) + render_json_dump(directory_columns: serialize_data(directory_columns, DirectoryColumnSerializer)) end end diff --git a/app/controllers/directory_items_controller.rb b/app/controllers/directory_items_controller.rb index 8f7314f75e8..b8ec391b9b0 100644 --- a/app/controllers/directory_items_controller.rb +++ b/app/controllers/directory_items_controller.rb @@ -26,13 +26,14 @@ class DirectoryItemsController < ApplicationController result = result.references(:user).where.not(users: { username: params[:exclude_usernames].split(",") }) end - order = params[:order] || DirectoryItem.headings.first + order = params[:order] || DirectoryColumn.automatic_column_names.first dir = params[:asc] ? 'ASC' : 'DESC' - if DirectoryItem.headings.include?(order.to_sym) + if DirectoryColumn.active_column_names.include?(order.to_sym) result = result.order("directory_items.#{order} #{dir}, directory_items.id") elsif params[:order] === 'username' result = result.order("users.#{order} #{dir}, directory_items.id") else + # Ordering by user field value user_field = UserField.find_by(name: params[:order]) if user_field result = result @@ -98,6 +99,10 @@ class DirectoryItemsController < ApplicationController serializer_opts[:user_field_ids] = params[:user_field_ids]&.split("|")&.map(&:to_i) end + if params[:plugin_column_ids] + serializer_opts[:plugin_column_ids] = params[:plugin_column_ids]&.split("|")&.map(&:to_i) + end + serialized = serialize_data(result, DirectoryItemSerializer, serializer_opts) render_json_dump(directory_items: serialized, meta: { diff --git a/app/controllers/edit_directory_columns_controller.rb b/app/controllers/edit_directory_columns_controller.rb new file mode 100644 index 00000000000..b40d13ce66e --- /dev/null +++ b/app/controllers/edit_directory_columns_controller.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +class EditDirectoryColumnsController < ApplicationController + requires_login + + def index + raise Discourse::NotFound unless guardian.is_staff? + + ensure_user_fields_have_columns + + columns = DirectoryColumn.includes(:user_field).all + render_json_dump(directory_columns: serialize_data(columns, EditDirectoryColumnSerializer)) + end + + def update + raise Discourse::NotFound unless guardian.is_staff? + params.require(:directory_columns) + directory_column_params = params.permit(directory_columns: {}) + directory_columns = DirectoryColumn.all + + has_enabled_column = directory_column_params[:directory_columns].values.any? do |column_data| + column_data[:enabled].to_s == "true" + end + raise Discourse::InvalidParameters, "Must have at least one column enabled" unless has_enabled_column + + directory_column_params[:directory_columns].values.each do |column_data| + existing_column = directory_columns.detect { |c| c.id == column_data[:id].to_i } + if (existing_column.enabled != column_data[:enabled] || existing_column.position != column_data[:position].to_i) + existing_column.update(enabled: column_data[:enabled], position: column_data[:position]) + end + end + + render json: success_json + end + + private + + def ensure_user_fields_have_columns + user_fields_without_column = + UserField.left_outer_joins(:directory_column) + .where(directory_column: { user_field_id: nil }) + .where("show_on_profile=? OR show_on_user_card=?", true, true) + + return unless user_fields_without_column.count > 0 + + next_position = DirectoryColumn.maximum("position") + 1 + + new_directory_column_attrs = [] + user_fields_without_column.each do |user_field| + new_directory_column_attrs.push({ + user_field_id: user_field.id, + enabled: false, + type: DirectoryColumn.types[:user_field], + position: next_position + }) + + next_position += 1 + end + + DirectoryColumn.insert_all(new_directory_column_attrs) + end +end diff --git a/app/models/directory_column.rb b/app/models/directory_column.rb index 4a3bc3546e0..8b73960a8d2 100644 --- a/app/models/directory_column.rb +++ b/app/models/directory_column.rb @@ -1,5 +1,52 @@ # frozen_string_literal: true class DirectoryColumn < ActiveRecord::Base + + # TODO(2021-06-18): Remove automatic column + self.ignored_columns = ["automatic"] + self.inheritance_column = nil + + enum type: { automatic: 0, user_field: 1, plugin: 2 } + + def self.automatic_column_names + @automatic_column_names ||= [:likes_received, + :likes_given, + :topics_entered, + :topic_count, + :post_count, + :posts_read, + :days_visited] + end + + def self.active_column_names + DirectoryColumn.where(type: [:automatic, :plugin]).where(enabled: true).pluck(:name).map(&:to_sym) + end + + @@plugin_directory_columns = [] + + def self.plugin_directory_columns + @@plugin_directory_columns + end + belongs_to :user_field + + def self.clear_plugin_directory_columns + @@plugin_directory_columns = [] + end + + def self.find_or_create_plugin_directory_column(attrs) + directory_column = find_or_create_by( + name: attrs[:column_name], + icon: attrs[:icon], + type: DirectoryColumn.types[:plugin] + ) do |column| + column.position = DirectoryColumn.maximum("position") + 1 + column.enabled = false + end + + unless @@plugin_directory_columns.include?(directory_column.name) + @@plugin_directory_columns << directory_column.name + DirectoryItem.add_plugin_query(attrs[:query]) + end + end end diff --git a/app/models/directory_item.rb b/app/models/directory_item.rb index 930c7829297..817697a434a 100644 --- a/app/models/directory_item.rb +++ b/app/models/directory_item.rb @@ -4,15 +4,7 @@ class DirectoryItem < ActiveRecord::Base belongs_to :user has_one :user_stat, foreign_key: :user_id, primary_key: :user_id - def self.headings - @headings ||= [:likes_received, - :likes_given, - :topics_entered, - :topic_count, - :post_count, - :posts_read, - :days_visited] - end + @@plugin_queries = [] def self.period_types @types ||= Enum.new(all: 1, @@ -34,8 +26,16 @@ class DirectoryItem < ActiveRecord::Base Time.zone.at(val.to_i) end - def self.refresh_period!(period_type, force: false) + def self.add_plugin_query(details) + @@plugin_queries << details + end + def self.clear_plugin_queries + @@plugin_queries = [] + end + + def self.refresh_period!(period_type, force: false) + DiscourseEvent.trigger("before_directory_refresh") Discourse.redis.set("directory_#{period_types[period_type]}", Time.zone.now.to_i) # Don't calculate it if the user directory is disabled @@ -53,30 +53,26 @@ class DirectoryItem < ActiveRecord::Base ActiveRecord::Base.transaction do # Delete records that belonged to users who have been deleted - DB.exec "DELETE FROM directory_items + DB.exec("DELETE FROM directory_items USING directory_items di LEFT JOIN users u ON (u.id = user_id AND u.active AND u.silenced_till IS NULL AND u.id > 0) WHERE di.id = directory_items.id AND u.id IS NULL AND - di.period_type = :period_type", period_type: period_types[period_type] + di.period_type = :period_type", period_type: period_types[period_type]) # Create new records for users who don't have one yet - DB.exec "INSERT INTO directory_items(period_type, user_id, likes_received, likes_given, topics_entered, days_visited, posts_read, topic_count, post_count) + + column_names = DirectoryColumn.automatic_column_names + DirectoryColumn.plugin_directory_columns + DB.exec("INSERT INTO directory_items(period_type, user_id, #{column_names.map(&:to_s).join(", ")}) SELECT :period_type, u.id, - 0, - 0, - 0, - 0, - 0, - 0, - 0 + #{Array.new(column_names.count) { |_| 0 }.join(", ") } FROM users u LEFT JOIN directory_items di ON di.user_id = u.id AND di.period_type = :period_type WHERE di.id IS NULL AND u.id > 0 AND u.silenced_till IS NULL AND u.active #{SiteSetting.must_approve_users ? 'AND u.approved' : ''} - ", period_type: period_types[period_type] + ", period_type: period_types[period_type]) # Calculate new values and update records # @@ -84,7 +80,18 @@ class DirectoryItem < ActiveRecord::Base # TODO # WARNING: post_count is a wrong name, it should be reply_count (excluding topic post) # - DB.exec "WITH x AS (SELECT + # + query_args = { + period_type: period_types[period_type], + since: since, + like_type: UserAction::LIKE, + was_liked_type: UserAction::WAS_LIKED, + new_topic_type: UserAction::NEW_TOPIC, + reply_type: UserAction::REPLY, + regular_post_type: Post.types[:regular] + } + + DB.exec("WITH x AS (SELECT u.id user_id, SUM(CASE WHEN p.id IS NOT NULL AND t.id IS NOT NULL AND ua.action_type = :was_liked_type THEN 1 ELSE 0 END) likes_received, SUM(CASE WHEN p.id IS NOT NULL AND t.id IS NOT NULL AND ua.action_type = :like_type THEN 1 ELSE 0 END) likes_given, @@ -123,14 +130,13 @@ class DirectoryItem < ActiveRecord::Base di.topic_count <> x.topic_count OR di.post_count <> x.post_count ) - ", - period_type: period_types[period_type], - since: since, - like_type: UserAction::LIKE, - was_liked_type: UserAction::WAS_LIKED, - new_topic_type: UserAction::NEW_TOPIC, - reply_type: UserAction::REPLY, - regular_post_type: Post.types[:regular] + ", + query_args + ) + + @@plugin_queries.each do |plugin_query| + DB.exec(plugin_query, query_args) + end if period_type == :all DB.exec <<~SQL diff --git a/app/serializers/directory_column_serializer.rb b/app/serializers/directory_column_serializer.rb index 18e18ba67b8..4e172d3e9e6 100644 --- a/app/serializers/directory_column_serializer.rb +++ b/app/serializers/directory_column_serializer.rb @@ -3,11 +3,12 @@ class DirectoryColumnSerializer < ApplicationSerializer attributes :id, :name, - :automatic, - :enabled, - :automatic_position, + :type, :position, - :icon + :icon, + :user_field_id - has_one :user_field, serializer: UserFieldSerializer, embed: :objects + def name + object.name || object.user_field.name + end end diff --git a/app/serializers/directory_item_serializer.rb b/app/serializers/directory_item_serializer.rb index 02a15ae3f47..1e18f84c802 100644 --- a/app/serializers/directory_item_serializer.rb +++ b/app/serializers/directory_item_serializer.rb @@ -20,7 +20,7 @@ class DirectoryItemSerializer < ApplicationSerializer :time_read has_one :user, embed: :objects, serializer: UserSerializer - attributes *DirectoryItem.headings + attributes *DirectoryColumn.active_column_names def id object.user_id diff --git a/app/serializers/edit_directory_column_serializer.rb b/app/serializers/edit_directory_column_serializer.rb new file mode 100644 index 00000000000..7c703d59659 --- /dev/null +++ b/app/serializers/edit_directory_column_serializer.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class EditDirectoryColumnSerializer < DirectoryColumnSerializer + attributes :enabled, + :automatic_position + + has_one :user_field, serializer: UserFieldSerializer, embed: :objects +end diff --git a/config/routes.rb b/config/routes.rb index 052480ef8ab..cd395847561 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -388,7 +388,8 @@ Discourse::Application.routes.draw do get "user-cards" => "users#cards", format: :json get "directory-columns" => "directory_columns#index", format: :json - put "directory-columns" => "directory_columns#update", format: :json + get "edit-directory-columns" => "edit_directory_columns#index", format: :json + put "edit-directory-columns" => "edit_directory_columns#update", format: :json %w{users u}.each_with_index do |root_path, index| get "#{root_path}" => "users#index", constraints: { format: 'html' } diff --git a/db/migrate/20210618135229_reintroduce_type_to_directory_columns.rb b/db/migrate/20210618135229_reintroduce_type_to_directory_columns.rb new file mode 100644 index 00000000000..bb149e7eebb --- /dev/null +++ b/db/migrate/20210618135229_reintroduce_type_to_directory_columns.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class ReintroduceTypeToDirectoryColumns < ActiveRecord::Migration[6.1] + def up + if !ActiveRecord::Base.connection.column_exists?(:directory_columns, :type) + # A migration that added this column was previously merged and reverted. + # Some sites have this column and some do not, so only add if missing. + add_column :directory_columns, :type, :integer, default: 0, null: false + end + + DB.exec( + <<~SQL + UPDATE directory_columns + SET type = CASE WHEN automatic THEN 0 ELSE 1 END; + SQL + ) + end + + def down + remove_column :directory_columns, :type + end +end diff --git a/lib/discourse_event.rb b/lib/discourse_event.rb index bd7a21a950d..b2e03b8b1d1 100644 --- a/lib/discourse_event.rb +++ b/lib/discourse_event.rb @@ -27,4 +27,7 @@ class DiscourseEvent events[event_name].delete(block) end + def self.all_off(event_name) + events.delete(event_name) + end end diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 2a92c6d01d6..646603effca 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -373,6 +373,14 @@ class Plugin::Instance assets end + def add_directory_column(column_name, query:, icon: nil) + validate_directory_column_name(column_name) + + DiscourseEvent.on("before_directory_refresh") do + DirectoryColumn.find_or_create_plugin_directory_column(column_name: column_name, icon: icon, query: query) + end + end + def delete_extra_automatic_assets(good_paths) return unless Dir.exists? auto_generated_path @@ -593,7 +601,6 @@ class Plugin::Instance # this allows us to present information about a plugin in the UI # prior to activations def activate! - if @path root_dir_name = File.dirname(@path) @@ -964,6 +971,11 @@ class Plugin::Instance private + def validate_directory_column_name(column_name) + match = /^[_a-z]+$/.match(column_name) + raise "Invalid directory column name '#{column_name}'. Can only contain a-z and underscores" unless match + end + def write_asset(path, contents) unless File.exists?(path) ensure_directory(path) diff --git a/spec/components/discourse_event_spec.rb b/spec/components/discourse_event_spec.rb index 4e87a079155..fe4ad080db8 100644 --- a/spec/components/discourse_event_spec.rb +++ b/spec/components/discourse_event_spec.rb @@ -83,9 +83,25 @@ describe DiscourseEvent do expect(harvey.job).to eq('Supervillain') expect(harvey.name).to eq('Two Face') end - end - end + context '#all_off' do + let(:event_handler_2) do + Proc.new { |user| user.job = 'Supervillain' } + end + + before do + DiscourseEvent.on(:acid_face, &event_handler_2) + end + + it 'removes all handlers with a key' do + harvey.job = 'gardening' + DiscourseEvent.all_off(:acid_face) + DiscourseEvent.trigger(:acid_face, harvey) # Doesn't change anything + expect(harvey.job).to eq('gardening') + end + + end + end end diff --git a/spec/components/plugin/instance_spec.rb b/spec/components/plugin/instance_spec.rb index e9de28e83d1..171e8c94b51 100644 --- a/spec/components/plugin/instance_spec.rb +++ b/spec/components/plugin/instance_spec.rb @@ -600,4 +600,51 @@ describe Plugin::Instance do expect(ApiKeyScope.scope_mappings.dig(:groups, :create, :actions)).to contain_exactly(*actions) end end + + describe '#add_directory_column' do + let!(:plugin) { Plugin::Instance.new } + + before do + DirectoryItem.clear_plugin_queries + end + + after do + DirectoryColumn.clear_plugin_directory_columns + end + + describe "with valid column name" do + let(:column_name) { "random_c" } + + before do + DB.exec("ALTER TABLE directory_items ADD COLUMN IF NOT EXISTS #{column_name} integer") + end + + after do + DB.exec("ALTER TABLE directory_items DROP COLUMN IF EXISTS #{column_name}") + DiscourseEvent.all_off("before_directory_refresh") + end + + it 'creates a directory column record when directory items are refreshed' do + plugin.add_directory_column(column_name, query: "SELECT COUNT(*) FROM users", icon: 'recycle') + expect(DirectoryColumn.find_by(name: column_name, icon: 'recycle', enabled: false)).not_to be_present + + DirectoryItem.refresh! + expect(DirectoryColumn.find_by(name: column_name, icon: 'recycle', enabled: false)).to be_present + end + end + + it 'errors when the column_name contains invalid characters' do + expect { + plugin.add_directory_column('Capital', query: "SELECT COUNT(*) FROM users", icon: 'recycle') + }.to raise_error(RuntimeError) + + expect { + plugin.add_directory_column('has space', query: "SELECT COUNT(*) FROM users", icon: 'recycle') + }.to raise_error(RuntimeError) + + expect { + plugin.add_directory_column('has_number_1', query: "SELECT COUNT(*) FROM users", icon: 'recycle') + }.to raise_error(RuntimeError) + end + end end diff --git a/spec/requests/admin/user_fields_controller_spec.rb b/spec/requests/admin/user_fields_controller_spec.rb index 828795859c6..9084ebf472a 100644 --- a/spec/requests/admin/user_fields_controller_spec.rb +++ b/spec/requests/admin/user_fields_controller_spec.rb @@ -130,7 +130,7 @@ describe Admin::UserFieldsController do DirectoryColumn.create( user_field_id: user_field.id, enabled: false, - automatic: false, + type: DirectoryColumn.types[:user_field], position: next_position ) expect { diff --git a/spec/requests/directory_columns_controller_spec.rb b/spec/requests/directory_columns_controller_spec.rb index 6f01eb01ad6..5fb8074b978 100644 --- a/spec/requests/directory_columns_controller_spec.rb +++ b/spec/requests/directory_columns_controller_spec.rb @@ -7,6 +7,17 @@ describe DirectoryColumnsController do fab!(:admin) { Fabricate(:admin) } describe "#index" do + it "returns all active directory columns" do + likes_given = DirectoryColumn.find_by(name: "likes_given") + likes_given.update(enabled: false) + + get "/directory-columns.json" + + expect(response.parsed_body["directory_columns"].map { |dc| dc["name"] }).not_to include("likes_given") + end + end + + describe "#edit-index" do fab!(:public_user_field) { Fabricate(:user_field, show_on_profile: true) } fab!(:private_user_field) { Fabricate(:user_field, show_on_profile: false, show_on_user_card: false) } @@ -14,13 +25,13 @@ describe DirectoryColumnsController do sign_in(admin) expect { - get "/directory-columns.json" + get "/edit-directory-columns.json" }.to change { DirectoryColumn.count }.by(1) end it "returns a 403 when not logged in as staff member" do sign_in(user) - get "/directory-columns.json" + get "/edit-directory-columns.json" expect(response.status).to eq(404) end @@ -50,7 +61,7 @@ describe DirectoryColumnsController do sign_in(admin) expect { - put "/directory-columns.json", params: params + put "/edit-directory-columns.json", params: params }.to change { DirectoryColumn.find(first_directory_column_id).enabled }.from(true).to(false) end @@ -59,14 +70,14 @@ describe DirectoryColumnsController do bad_params = params bad_params[:directory_columns][:"1"][:enabled] = false - put "/directory-columns.json", params: bad_params + put "/edit-directory-columns.json", params: bad_params expect(response.status).to eq(400) end it "returns a 404 when not logged in as a staff member" do sign_in(user) - put "/directory-columns.json", params: params + put "/edit-directory-columns.json", params: params expect(response.status).to eq(404) end
{{i18n "groups.requests.reason"}}