تنفيذ التحقق من حقول المستخدم المخصصة

هذه مقالة للمطورين المستقبليين الذين قد يحتاجون إليها لإنشاء إضافات مخصصة خصيصًا للتحقق من حقول المستخدم المخصصة.

تمت كتابتها أثناء استخدام إصدار Discourse 3.6.0.beta3-latest (الالتزام الحالي a7326abf15)، لذلك إذا لم ينجح شيء ما، فقد يكون ذلك بسبب تغيير في الكود الأساسي.

السبب الرئيسي لكتابة هذه المقالة هو الغياب التام للمعلومات المتعلقة بإضافة تحقق مخصص لحقول المستخدم المخصصة.

القصة القصيرة هي أنني اضطررت إلى إضافة تحقق مخصص لأحد حقول المستخدم المخصصة، وهو في الأساس حقل إدخال نصي مجاني. كان المطلب الرئيسي هو جعل هذا الحقل يحتوي على قيمة فريدة. بينما كنت مطور Python/PHP، كان من السهل جدًا تنفيذه على الأطر/أنظمة إدارة المحتوى التي أعمل بها، بينما لم يكن 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

الآن، المشكلة الرئيسية التي واجهتها كانت كيفية إدخالها في الحقول المخصصة على الإطلاق. بعد البحث في الكود الأساسي، وجدت واجهة برمجة التطبيقات 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) => {
  // 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 هو معرف الحقل، يمكن التحقق منه أثناء فتح صفحة تحرير للحقل المخصص ضمن عنوان URL.
  • isValueUnique هي دالة مخصصة حيث تقوم جميع منطقك المخصص بالتحقق. يجب تحديدها خارج api.addCustomUserFieldValidationCallback.

نظرًا لأننا بحاجة إلى التحقق من القيمة في قاعدة البيانات، فهذا يعني أننا بحاجة إما إلى إجراء استدعاء لواجهة برمجة التطبيقات إلى نقطة نهاية معينة ستقوم بالتحقق وتقديم استجابة لنا إذا كانت صالحة أم لا، أو قد تكون هناك طريقة أخرى للتحقق من ذلك دون نقطة نهاية مخصصة لم أجدها بعد. هناك وحدة لبعض التحقق، ولكن التحقق الخاص بها مقيد بفحص REGEX مباشرة في JS، وهو ما لم أكن بحاجة إليه.

ملف 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

الآن الجزء الصعب، يمكنك استخدام نقطة النهاية هذه وإجراء استدعاء fetch/ajax من المُهيئ الخاص بك، لكنها لن تعمل، والسبب الرئيسي هو أن addCustomUserFieldValidationCallback لا تعمل مع ردود الاتصال غير المتزامنة، لذا تحتاج إلى إجراء استدعاء غير غير متزامن. لقد استخدمت XMLHttpRequest لهذا الغرض، كمثال xhr.open('GET', '/my_custom_plugin_endpoint_url/${fieldId}', true); حيث true يعطل الوضع غير المتزامن.
سيبدأ هذا في العمل، لكنك ستواجه مشكلة، حيث لا يمكنك الكتابة بسرعة في حقل الإدخال، لأن addCustomUserFieldValidationCallback يتم تشغيلها مع كل حرف يتم توفيره وستنتظر حتى تحصل على نتيجة صحيحة، مما يؤدي إلى تجميد حقل الإدخال الخاص بك.

لم أظهره في الأمثلة، لكن حلي كان استرداد قائمة القيم الفريدة للحقل بعد تحميل الصفحة واستخدامها لتصفية القيمة مباشرة في JS دون إجراء استدعاءات إضافية لواجهة برمجة التطبيقات، تحتاج فقط إلى التأكد من أن لديك عددًا معقولًا من القيم وتأمين نقطة النهاية. في حالتي، بدلاً من قائمة القيم، قمت بإرجاع قائمة تجزئات، لذلك في JS أقوم بتحويل القيمة المكتوبة إلى تجزئة ومقارنة التجزئات. بالإضافة إلى حقيقة أنها أكثر أمانًا قليلاً، فإنها تقلل أيضًا من وزن الاستجابة بشكل كبير، بحيث يمكنك الحصول على المزيد من القيم للمقارنة.

هنا المعلومات المتعلقة بمجلد الإعدادات.

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. ما فعلته هو إضافة إضافتي المخصصة في مجلد/مستودع docker، الذي يحتوي على إعداد Discourse وقم بتثبيته أثناء عملية إعادة البناء في مجلد الإضافات داخل الحاوية.

آمل أن يساعد هذا شخصًا ما في تنفيذ عمليات تحقق مخصصة لحقول المستخدم.

إعجاب واحد (1)