ユーザーカスタムフィールドの検証実装

カスタムプラグインを特にユーザーカスタムフィールドの検証用に作成する必要がある将来のDEV向けのガイドです。

Discourse バージョン 3.6.0.beta3-latest (現在のコミット a7326abf15) で記述されているため、何かが機能しない場合は、コアコードが変更された可能性があります。

この記事を書いた主な理由は、ユーザーカスタムフィールドにカスタム検証を追加することに関する情報が全くないことです。

短い話は、カスタム検証をユーザーカスタムフィールドの1つに追加する必要があったということです。これは基本的にフリーテキスト入力です。主な要件は、このフィールドが一意の値を持つようにすることでした。Python/PHP開発者としては、私が作業しているフレームワーク/CMSで実装するのは非常に簡単ですが、Discourseはそうではなく、言語固有の問題ではなく、これを行うためのドキュメントがなく、同様の質問で役立つ可能性のある投稿が見つかったものの、回答されなかったという事実です。コアリサーチに半日費やした後、ついに期待どおりの結果が得られたので、誰かの役に立つかもしれないので、他の人と共有したいと思います。

したがって、一意の値を持つ必要があるユーザーカスタムフィールド。

これを解決するには、Discourseのカスタムプラグインを作成する必要があります。主な理由は、DBで値をチェックする必要があるためです。これはバックエンドで行われます。

構造は次のとおりです。

- 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 ファイルにあります。これは、サインアップフォームの最初の送信試行後にトリガーされ、その後、入力またはユーザーカスタムフィールドのいずれかにタイプするたびにトリガーされるメソッドであり、私が求めていたものです。
このメソッドの例では、コールバック関数を受け入れ、返される引数は、現在操作しているユーザーフィールド自体であることが示されています。

これは your-initializer-js-file.js のコードです(完全ではありませんが十分です)。

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

export default apiInitializer("1.8.0", (api) => {
  // List of field IDs that require unique validation
  const UNIQUE_FIELD_IDS = ["5"];

  // Client-side validation callback
  api.addCustomUserFieldValidationCallback((userField) => {
    const fieldId = userField.field.id.toString();

    // Only validate fields in our unique fields list
    if (UNIQUE_FIELD_IDS.includes(fieldId)) {
      // Check if value is empty
      if (!userField.value || userField.value.trim() === "") {
        return null; // Let default validation handle empty values
      }

      const value = userField.value.trim();

      // Check against values list
      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 の外に指定する必要があります。

DBで値をチェックする必要があるため、検証を実行し、有効かどうかを応答するAPIエンドポイントに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
  # Hardcoded list of unique field IDs
  UNIQUE_FIELD_IDS = ["5"].freeze

  # Add API endpoints for validation
  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_")

      # Normalize the value: trim whitespace and compare case-insensitively
      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

さて、ここがトリッキーな部分です。このエンドポイントを使用して、イニシャライザーからフェッチ/AJAX呼び出しを行うことができますが、機能しません。主な理由は、addCustomUserFieldValidationCallback非同期コールバックでは機能しないため、非同期ではない呼び出しを行う必要があります。例として XMLHttpRequest を使用しました。xhr.open('GET', '/my_custom_plugin_endpoint_url/${fieldId}', true); ここで true は非同期を無効にします。
これは機能し始めますが、addCustomUserFieldValidationCallback が提供された文字ごとにトリガーされ、適切な戻り値が得られるまで待機するため、入力がフリーズするという問題に直面します。

例では示していませんが、私の解決策は、ページロード後にフィールドの一意の値リストを取得し、追加のAPI呼び出しを行わずにJSで直接値をフィルタリングすることでした。値の数が妥当であり、エンドポイントが保護されていることを確認するだけで済みます。私の場合は、値リストの代わりにハッシュリストを返したので、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"

追加:Dockerセットアップがあるので、app.yml にカスタムプラグインを追加すること以外に、カスタムプラグインを追加する方法が見つかりませんでした。私がやったことは、Discourseのセットアップを含むdockerフォルダ/リポジトリにカスタムプラグインを追加し、コンテナ内のプラグインフォルダにマウントしながら再ビルド操作を行ったことです。

これが、ユーザーフィールドのカスタム検証を実装するのに役立つことを願っています。

「いいね!」 1