Это статья для будущих разработчиков, которым может понадобиться эта информация для создания собственного плагина, специально предназначенного для валидации пользовательских полей.
Статья написана на базе версии Discourse 3.6.0.beta3-latest (текущий коммит a7326abf15), поэтому, если что-то не будет работать, возможно, в коде ядра (Core) что-то изменилось.
Главная причина написания этой статьи — полное отсутствие информации о добавлении кастомной валидации для пользовательских полей.
Если коротко: мне пришлось добавить кастомную валидацию для одного из пользовательских полей, которое по сути является текстовым полем ввода. Основное требование заключалось в том, чтобы значение этого поля было уникальным. Будучи разработчиком на 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 в ядре. Это метод, который срабатывает после первой попытки отправки формы регистрации, а затем вызывается при каждом вводе символа в поле или любое другое пользовательское поле, что мне и требовалось.
В примере для этого метода показано, что он принимает функцию обратного вызова (callback), а аргумент, который он возвращает, — это сам объект 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"];
// Клиентский callback для валидации
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-запрос к какому-то эндпоинту, который выполнит валидацию и вернет ответ о её результате, либо существует другой способ проверки без кастомного эндпоинта, который я пока не нашел. Существует модуль для некоторой валидации, но его валидация ограничена проверкой по 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
# Закодированный список 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
Теперь самая хитрая часть: вы можете использовать этот эндпоинт и делать fetch/ajax-запрос из вашего инициализатора, но это не сработает. Основная причина в том, что addCustomUserFieldValidationCallback не работает с асинхронными callback-функциями, поэтому вам нужно сделать запрос, который не будет асинхронным. Для этого я использовал 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. Я добавил свой кастомный плагин в папку/репозиторий Docker, содержащий настройку Discourse, и подключил его при операции пересборки в папку плагинов внутри контейнера.
Надеюсь, это поможет кому-то реализовать кастомную валидацию для пользовательских полей.