Implémentation de la validation des champs personnalisés utilisateur

Ceci est un article destiné aux futurs développeurs qui pourraient en avoir besoin pour créer des plugins personnalisés spécifiquement pour la validation des champs personnalisés des utilisateurs.

Rédigé avec la version 3.6.0.beta3-latest de Discourse (commit actuel a7326abf15), donc si quelque chose ne fonctionne pas, il est possible que quelque chose ait changé dans le code principal.

La raison principale de la rédaction de cet article est l’absence totale d’informations concernant l’ajout de validations personnalisées aux champs personnalisés des utilisateurs.

L’histoire courte est que j’ai dû ajouter une validation personnalisée pour l’un des champs personnalisés de l’utilisateur, qui est essentiellement une saisie de texte libre. L’exigence principale était de rendre ce champ unique. En tant que développeur Python/PHP, il est assez facile à implémenter sur les frameworks/CMS avec lesquels je travaille, alors que Discourse n’était pas le cas et il ne s’agit pas des spécificités du langage, mais du fait qu’il n’y a pas de documentation pour le faire et que les publications que j’ai trouvées avec des questions similaires qui pourraient aider, n’ont pas reçu de réponse. Après une demi-semaine de recherche dans le code source, j’ai finalement obtenu le résultat attendu et je veux le partager avec d’autres, car cela pourrait aider quelqu’un.

Donc, un champ personnalisé d’utilisateur qui doit avoir une valeur unique.

Pour résoudre ce problème, nous devons créer un plugin personnalisé pour Discourse, la raison principale étant que nous devons vérifier la valeur dans la base de données, ce qui se fait côté serveur.

Voici la structure :

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

Maintenant, le problème principal que j’ai rencontré était de savoir comment injecter dans les champs personnalisés. Après avoir recherché le code source, j’ai trouvé la méthode API addCustomUserFieldValidationCallback, elle se trouve dans le fichier plugin-api.gjs du code source. C’est une méthode qui se déclenche après la première tentative de soumission du formulaire d’inscription et qui se déclenche ensuite à chaque frappe dans le champ de saisie ou dans n’importe quel champ personnalisé de l’utilisateur, ce qui est ce dont j’avais besoin.
Dans l’exemple de cette méthode, il est indiqué qu’elle accepte une fonction de rappel et l’argument qu’elle vous renvoie est le champ utilisateur lui-même avec lequel vous interagissez au moment présent.
Voici le code pour your-initializer-js-file.js (pas complet mais suffisant) :

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

export default apiInitializer("1.8.0", (api) => {
  // Liste des identifiants de champs qui nécessitent une validation unique
  const UNIQUE_FIELD_IDS = ["5"];

  // Fonction de rappel de validation côté client
  api.addCustomUserFieldValidationCallback((userField) => {
    const fieldId = userField.field.id.toString();

    // Valider uniquement les champs de notre liste de champs uniques
    if (UNIQUE_FIELD_IDS.includes(fieldId)) {
      // Vérifier si la valeur est vide
      if (!userField.value || userField.value.trim() === "") {
        return null; // Laisser la validation par défaut gérer les valeurs vides
      }

      const value = userField.value.trim();

      // Vérifier par rapport à la liste des valeurs
      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;
  });
});

Points principaux à commenter ici :

  • UNIQUE_FIELD_IDS est utilisé pour filtrer quel champ je veux qu’il soit affecté par ma logique personnalisée. Le nombre 5 est l’ID du champ, il peut être vérifié en ouvrant la page Modifier du champ personnalisé dans l’URL.
  • isValueUnique est une fonction personnalisée où toute votre logique personnalisée effectue la validation. Elle doit être spécifiée en dehors de api.addCustomUserFieldValidationCallback.

Parce que nous devons vérifier la valeur dans la base de données, cela signifie que nous devons soit faire un appel API vers un point de terminaison qui effectuera la validation et nous fournira une réponse si elle est valide ou non, soit il pourrait y avoir une autre façon de le vérifier sans point de terminaison personnalisé que je n’ai pas encore trouvé. Il existe un module pour certaines validations, mais sa validation est limitée à une vérification REGEX directement en JS, ce qui n’était pas ce dont j’avais besoin.

Le fichier plugin.rb est l’endroit où nous spécifions les informations concernant notre plugin personnalisé, ses configurations et tout ce qui peut être ajouté. Dans mon cas, j’enregistre mon point de terminaison personnalisé pour la validation. Voici un exemple de validation possible, vous pourriez l’avoir différemment.

# frozen_string_literal: true

# name: my_custom_plugin
# about: Valide l'unicité des champs personnalisés des utilisateurs
# version: 0.1
# authors: Yan R

enabled_site_setting :my_custom_plugin_enabled

after_initialize do
  # Liste codée en dur des identifiants de champs uniques
  UNIQUE_FIELD_IDS = ["5"].freeze

  # Ajouter des points de terminaison API pour la 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_")

      # Normaliser la valeur : supprimer les espaces et comparer sans tenir compte de la casse
      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

Maintenant, voici la partie délicate, vous pouvez utiliser ce point de terminaison et faire un appel fetch/ajax depuis votre initialiseur, mais cela ne fonctionnera pas, la raison principale est que addCustomUserFieldValidationCallback ne fonctionne pas avec des rappels asynchrones, vous devez donc faire un appel qui ne sera pas asynchrone. J’ai utilisé XMLHttpRequest pour cela, par exemple xhr.open('GET', '/my_custom_plugin_endpoint_url/${fieldId}', true);true désactive l’asynchrone.
Cela commencera à fonctionner, mais vous rencontrerez un problème, où vous ne pourrez pas taper rapidement dans le champ de saisie, car addCustomUserFieldValidationCallback se déclenche à chaque lettre fournie et attendra qu’il ait un retour approprié, ce qui bloque votre saisie.

Je ne l’ai pas montré dans les exemples, mais ma solution a été de récupérer la liste des valeurs uniques du champ après le chargement de la page et de l’utiliser pour filtrer la valeur directement en JS sans faire d’appels API supplémentaires, il vous suffit de vous assurer que vous avez un nombre raisonnable de valeurs et de sécuriser le point de terminaison. Dans mon cas, au lieu de la liste des valeurs, j’ai renvoyé une liste de hachages, donc en JS, je transforme la valeur tapée en hachage et je compare les hachages. Outre le fait que c’est un peu plus sécurisé, cela réduit également considérablement le poids de la réponse, vous pouvez donc obtenir beaucoup plus de valeurs à comparer.

Voici les informations concernant le dossier de configuration.

settings.yml :

plugins:
  my_custom_plugin_enabled:
    default: true
    client: true

server.en.yml :

en:
  site_settings:
    my_custom_plugin_enabled: "Activer la validation des champs d'utilisateur uniques"
  my_custom_plugin_text_validator:
    value_taken: "Cette valeur est déjà prise"

client.en.yml :

en:
  js:
    my_custom_plugin_text_validator:
      value_taken: "Cette valeur est déjà prise"

Addition : Je n’ai pas trouvé d’informations sur la façon d’ajouter mon plugin personnalisé en dehors de l’ajouter dans app.yml, car j’ai une configuration Docker. Ce que j’ai fait, c’est ajouter mon plugin personnalisé dans le dossier/dépôt docker, qui contient la configuration de Discourse et l’ai monté lors de l’opération de reconstruction dans le dossier du plugin à l’intérieur du conteneur.

J’espère que cela aidera quelqu’un à implémenter des validations personnalisées pour les champs d’utilisateur.

1 « J'aime »