用户自定义字段验证实现

这篇面向未来开发者(DEV)的文章,旨在帮助他们创建专门用于验证用户自定义字段的自定义插件。

撰写本文时,Discourse 版本为 3.6.0.beta3-latest(当前提交 a7326abf15)。因此,如果某些功能不起作用,可能是因为核心代码已发生更改。

撰写本文的主要原因是,关于为用户自定义字段添加自定义验证的信息完全缺失。

简而言之,我需要为用户自定义字段添加一个自定义验证,该字段基本上是一个自由文本输入。主要要求是使该字段具有唯一值。作为一名 Python/PHP 开发者,在我的工作框架/CMS 中实现这一点相当容易,但 Discourse 并非如此,而且这与语言的特定性无关,而是因为没有关于如何执行此操作的文档,并且我找到的关于类似问题的帖子都没有得到解答。经过半周的核心代码研究,我终于取得了预期的结果,并想与大家分享,希望能有所帮助。

因此,用户自定义字段应具有唯一值。

要解决此问题,我们需要创建一个 Discourse 的自定义插件,主要原因是我们需要在数据库中检查该值,而这在后端完成。

这是结构:

- my_custom_plugin
-- assets
--- javascripts
---- discourse
----- initializers
------ your-initializer-js-file.js
-- config
--- locales
---- server.en.yml
---- client.en.yml
--- settings.yml
-- plugin.rb

现在,我遇到的主要问题是如何完全注入自定义字段。在搜索核心代码后,我发现了 api 方法 addCustomUserFieldValidationCallback,它可以在核心代码的 plugin-api.gjs 文件中找到。此方法在首次尝试提交注册表单后触发,之后在每次输入或任何用户自定义字段的输入时触发,这正是我需要的。
此方法的示例显示它接受一个 回调函数,并返回您当前正在交互的 userField 作为参数。
以下是 your-initializer-js-file.js 的代码(并非完整,但足够):

import { apiInitializer } from "discourse/lib/api";
import { i18n } from "discourse-i18n";

export default apiInitializer("1.8.0", (api) => {
  // 需要唯一验证的字段 ID 列表
  const UNIQUE_FIELD_IDS = ["5"];

  // 客户端验证回调
  api.addCustomUserFieldValidationCallback((userField) => {
    const fieldId = userField.field.id.toString();

    // 仅验证我们唯一字段列表中的字段
    if (UNIQUE_FIELD_IDS.includes(fieldId)) {
      // 检查值是否为空
      if (!userField.value || userField.value.trim() === "") {
        return null; // 让默认验证处理空值
      }

      const value = userField.value.trim();

      // 与值列表进行检查
      const unique = isValueUnique(fieldId, value);
      if (!unique) {
        return {
          failed: true,
          ok: false,
          reason: i18n("js.my_custom_plugin_text_validator.value_taken"),
          element: userField.field.element,
        };
      }
    }

    return null;
  });
});

此处的主要注释点:

  • UNIQUE_FIELD_IDS 用于过滤我想要应用自定义逻辑的字段。数字 5 是字段 ID,可以在自定义字段的 编辑页面 的 URL 中查看。
  • isValueUnique 是一个自定义函数,您所有的自定义逻辑都在其中进行验证。它应该在 api.addCustomUserFieldValidationCallback 之外指定。

因为我们需要在数据库中检查该值,这意味着我们需要调用一个执行验证并提供有效性响应的 API 端点,或者可能存在另一种无需自定义端点即可检查此值的方法,但我尚未找到。有一个用于某些 验证 的模块,但其验证仅限于 JS 中的 REGEX 检查,这并非我所需要的。

plugin.rb 文件用于指定有关我们自定义插件的信息、其配置以及所有可能添加的内容。在我的例子中,我注册了我的自定义验证端点。以下是可能的验证示例,您可能有所不同。

# frozen_string_literal: true

# name: my_custom_plugin
# about: Validates uniqueness of custom user fields
# version: 0.1
# authors: Yan R

enabled_site_setting :my_custom_plugin_enabled

after_initialize do
  # 硬编码的唯一字段 ID 列表
  UNIQUE_FIELD_IDS = ["5"].freeze

  # 添加用于验证的 API 端点
  Discourse::Application.routes.append do
    get "/my_custom_plugin_endpoint_url/:field_id" => "my_custom_plugin_validation#validate"
  end

  class ::MyCustomPluginValidationController < ::ApplicationController
    skip_before_action :verify_authenticity_token
    skip_before_action :check_xhr
    skip_before_action :redirect_to_login_if_required

    def validate
      field_name = params[:field_name]
      field_value = params[:field_value]

      return render json: { valid: true } unless field_name.present? && field_value.present?
      return render json: { valid: true } unless field_name.start_with?("user_field_")

      # 规范化值:去除空格并进行不区分大小写的比较
      normalized_value = field_value.to_s.strip
      return render json: { valid: true } if normalized_value.empty?

      existing = UserCustomField
        .where(name: field_name)
        .where("LOWER(TRIM(value)) = LOWER(?)", normalized_value)
        .exists?

      render json: { valid: !existing }
    end
  end
end

现在,棘手的部分来了,您可以使用此端点并从您的初始化器中发出 fetch/ajax 调用,但这将不起作用,主要原因是 addCustomUserFieldValidationCallback 不支持 异步回调,因此您需要进行一个非异步的调用。我以 XMLHttpRequest 为例,例如 xhr.open('GET', '/my_custom_plugin_endpoint_url/${fieldId}', true); 其中 true 禁用异步。
这将开始工作,但您会遇到一个问题,即无法快速在输入框中键入,因为 addCustomUserFieldValidationCallback 会在每个输入的字母时触发并等待正确返回,从而冻结您的输入。

我在示例中没有显示,但我的解决方案是在页面加载后检索字段的唯一值列表,并使用它直接在 JS 中过滤该值,而无需进行额外的 API 调用。您只需确保您有合理数量的值,并保护好端点。在我的例子中,我返回的是哈希列表而不是值列表,所以在 JS 中,我将输入的值转换为哈希并比较哈希。除了稍微更安全之外,它还显著减小了响应的大小,因此您可以获取更多值进行比较。

以下是关于 config 文件夹的信息。

settings.yml

plugins:
  my_custom_plugin_enabled:
    default: true
    client: true

server.en.yml

en:
  site_settings:
    my_custom_plugin_enabled: "Enable unique user fields validation"
  my_custom_plugin_text_validator:
    value_taken: "This value is already taken"

client.en.yml

en:
  js:
    my_custom_plugin_text_validator:
      value_taken: "This value is already taken"

补充:我没有找到除了在 app.yml 中添加自定义插件的信息,因为我使用的是 Docker 设置。我所做的是将我的自定义插件添加到包含 Discourse 设置的 docker 文件夹/仓库中,并在容器内的插件文件夹中进行重建操作。

希望这能帮助一些人实现用户字段的自定义验证。

1 个赞