Implementazione della convalida dei campi personalizzati dell'utente

Questo è un articolo per futuri sviluppatori che potrebbero averne bisogno per la creazione di plugin personalizzati specificamente per la convalida dei campi utente personalizzati.

Scritto mentre si utilizza la versione 3.6.0.beta3-latest di Discourse (commit corrente a7326abf15), quindi se qualcosa non dovesse funzionare, potrebbe essere che qualcosa sia cambiato nel codice Core.

Il motivo principale per cui scrivo questo è l’assoluta mancanza di informazioni riguardo all’aggiunta di convalide personalizzate ai campi utente personalizzati.

La breve storia è che ho dovuto aggiungere una convalida personalizzata per uno dei campi utente personalizzati, che è fondamentalmente un campo di testo libero. Il requisito principale era rendere questo campo univoco. Essendo uno sviluppatore Python/PHP, è abbastanza facile da implementare nei framework/CMS con cui lavoro, mentre Discourse non lo era e non si tratta di specificità del linguaggio, ma del fatto che non c’è documentazione per farlo e i post che ho trovato con domande simili che potrebbero aiutare, non hanno ricevuto risposta. Dopo mezza settimana di ricerca nel Core, ho finalmente ottenuto il risultato atteso e voglio condividerlo con altri, poiché potrebbe aiutare qualcuno.

Quindi, un campo utente personalizzato che dovrebbe avere un valore univoco.

Per risolvere questo problema, dobbiamo creare un plugin personalizzato per Discourse, il motivo principale è che dobbiamo controllare il valore nel DB, cosa che viene fatta nel backend.

Ecco la struttura:

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

Ora il problema principale che ho affrontato è stato come iniettare nei campi personalizzati. Dopo aver cercato nel Core, ho trovato il metodo API addCustomUserFieldValidationCallback, che si trova nel file plugin-api.gjs nel Core. Questo è un metodo che viene attivato dopo il primo tentativo di invio del modulo di registrazione e successivamente viene attivato ad ogni digitazione nell’input o in qualsiasi campo utente personalizzato, che è ciò di cui avevo bisogno.
Nell’esempio per questo metodo, mostra che accetta una funzione di callback e l’argomento che ti restituisce è il userField stesso con cui stai interagendo al momento.
Ecco il codice per your-initializer-js-file.js (non completo ma sufficiente):

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

export default apiInitializer("1.8.0", (api) => {
  // Elenco degli ID dei campi che richiedono una convalida univoca
  const UNIQUE_FIELD_IDS = ["5"];

  // Callback di convalida lato client
  api.addCustomUserFieldValidationCallback((userField) => {
    const fieldId = userField.field.id.toString();

    // Convalida solo i campi nel nostro elenco di campi univoci
    if (UNIQUE_FIELD_IDS.includes(fieldId)) {
      // Controlla se il valore è vuoto
      if (!userField.value || userField.value.trim() === "") {
        return null; // Lascia che la convalida predefinita gestisca i valori vuoti
      }

      const value = userField.value.trim();

      // Controlla rispetto all'elenco dei valori
      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;
  });
});

Punti principali da commentare qui:

  • UNIQUE_FIELD_IDS viene utilizzato per filtrare quale campo voglio che sia influenzato dalla mia logica personalizzata. Il numero 5 è l’ID del campo, che può essere controllato aprendo la pagina Modifica per il campo personalizzato nell’URL.
  • isValueUnique è una funzione personalizzata in cui tutta la tua logica personalizzata esegue la convalida. Dovrebbe essere specificata al di fuori di api.addCustomUserFieldValidationCallback.

Poiché dobbiamo controllare il valore nel DB, ciò significa che dobbiamo effettuare una chiamata API a un endpoint che eseguirà la convalida e ci fornirà una risposta se è valido o meno, OPPURE potrebbe esserci un altro modo per verificarlo senza un endpoint personalizzato che non ho ancora trovato. Esiste un modulo per alcune convalidi, ma la sua convalida è limitata a controlli REGEX direttamente in JS, che non è ciò di cui avevo bisogno.

Il file plugin.rb è dove specifichiamo le informazioni riguardanti il nostro plugin personalizzato, le sue configurazioni e tutto ciò che è possibile aggiungere. Nel mio caso, registro il mio endpoint personalizzato per la convalida. Ecco un esempio di possibile convalida, potresti averla diversamente.

# frozen_string_literal: true

# name: my_custom_plugin
# about: Convalida l'unicità dei campi utente personalizzati
# version: 0.1
# authors: Yan R

enabled_site_setting :my_custom_plugin_enabled

after_initialize do
  # Elenco hardcoded degli ID dei campi univoci
  UNIQUE_FIELD_IDS = ["5"].freeze

  # Aggiungi endpoint API per la convalida
  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_")

      # Normalizza il valore: rimuovi spazi bianchi e confronta senza distinzione tra maiuscole e minuscole
      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

Ora la parte complicata, puoi usare questo endpoint ed effettuare una chiamata fetch/ajax dal tuo initializer, ma non funzionerà, il motivo principale è che addCustomUserFieldValidationCallback non funziona con callback asincroni, quindi devi effettuare una chiamata che non sia asincrona. Ho usato XMLHttpRequest per questo, ad esempio xhr.open('GET', '/my_custom_plugin_endpoint_url/${fieldId}', true); dove true disabilita l’asincrono.
Questo inizierà a funzionare, ma incontrerai un problema, dove non puoi digitare velocemente nell’input, perché addCustomUserFieldValidationCallback viene attivato con ogni lettera fornita e attenderà finché non avrà un ritorno corretto, quindi bloccherà il tuo input.

Non l’ho mostrato negli esempi, ma la mia soluzione è stata recuperare l’elenco dei valori univoci dei campi dopo il caricamento della pagina e usarlo per filtrare il valore direttamente in JS senza effettuare chiamate API aggiuntive, devi solo assicurarti di avere una quantità ragionevole di valori e proteggere l’endpoint. Nel mio caso, invece dell’elenco dei valori, ho restituito un elenco di hash, quindi in JS trasformo il valore digitato in un hash e confronto gli hash. Oltre al fatto che è un po’ più sicuro, riduce anche significativamente il peso della risposta, quindi puoi ottenere molti più valori da confrontare.

Ecco le informazioni riguardanti la cartella di configurazione.

settings.yml:

plugins:
  my_custom_plugin_enabled:
    default: true
    client: true

server.en.yml:

en:
  site_settings:
    my_custom_plugin_enabled: "Abilita la convalida dei campi utente univoci"
  my_custom_plugin_text_validator:
    value_taken: "Questo valore è già stato preso"

client.en.yml:

en:
  js:
    my_custom_plugin_text_validator:
      value_taken: "Questo valore è già stato preso"

Aggiunta: Non ho trovato informazioni su come aggiungere il mio plugin personalizzato oltre ad aggiungerlo in app.yml, poiché ho una configurazione Docker. Quello che ho fatto è stato aggiungere il mio plugin personalizzato nella cartella/repository docker, che contiene la configurazione di Discourse e l’ho montato durante l’operazione di Ricompilazione nella cartella dei plugin all’interno del Container.

Spero che questo aiuti qualcuno a implementare convalide personalizzate per i campi utente.

1 Mi Piace