Comment ajouter des champs personnalisés aux modèles

This is a collection of education plugins that demonstrate how to add a custom field to different models in Discourse. They are intended as learning tools for those looking to learn how to build discourse plugins.

GitHub-Mark How to add a custom field to a topic
GitHub-Mark How to add a custom field to a category

Who they’re for

These plugins are for people looking to learn more about creating Discourse plugins. Before you start working with these plugins, you should complete the beginners guide to creating discourse plugins.

You could use these plugins just to add custom fields on your Discourse instance, however you would still need to change the code slightly to do so. It’s not designed for plugin-and-play use on a live server.

How they work

As well as containing working code, each plugin contains a step-by-step description of what the code is doing in the form of comments. For example

## 
# type:        step
# number:      1
# title:       Register the field
# description: Where we tell discourse what kind of field we're adding.
#              You can register a string, integer, boolean or json field.
# references:  lib/plugins/instance.rb,
#              app/models/concerns/has_custom_fields.rb
##
register_topic_custom_field_type(FIELD_NAME, FIELD_TYPE.to_sym)

Hopefully the steps and comments are self explanatory. The references are there to show you where to look if you want to learn more.

Let me know if you find it useful, if something isn’t working, or the notes are unclear :slight_smile:

28 « J'aime »

Thank for build plugin cho our,
i have this error later install plugin:

Oops
The software powering this discussion forum encountered an unexpected problem. We apologize for the inconvenience.
Detailed information about the error was logged, and an automatic notification generated. We’ll take a look at it.
No further action is necessary. However, if the error condition persists, you can provide additional detail, including steps to reproduce the error, by posting a discussion topic in the site’s feedback category.

Hey, are you running this in your local development environment? If so, could you PM your development logs? If your dev environment is set up correctly, this plugin will work.

1 « J'aime »

I read, but i can’t catch it. Can you spending time make a video tutorial install this plugin from make local development environment to step by step add a custom field …
So difficult to access this plugin.
If possible, can you upgrade search feature for field type is number?

I can donate for this plugin, please support help me!

This resource that @angus has put together is even more helpful than I first realized.

Not only is the code for adding a custom field there with clear explanations, but a lot of it can be plugged directly into any plugin, because the code mostly uses variables like FIELD_NAME and FIELD_VALUE, which you can define in plugin/config/settings.yml (you also need to make sure that your plugin file structure is the same as in the github code @angus has provided). Going through the code has also given me a greater understanding of some discourse functions and methods I have seen before, but never really understood until now.

So far, the code works great to create and save topic custom fields. There are two questions that have been coming up for me:

  1. Topic List Error: It seems to throw an error in the case I try to load a category list (ie, a topic list for a category) where there are topics created prior to adding the custom field. It shows the exception page, and lists this error:
    Attempting to access a non preloaded custom field, this is disallowed to prevent N+1 queries. What’s the recommended way to solve this one?

  2. Is there a way for me to apply the custom field only for topics in certain categories? So, let’s say I have Category 1, Category 2, and Category 3, and I only want the custom field input to show up and only want the field to be saved if the topic is part of Category 3. Is there a way to do that?

1 « J'aime »

We (Pavilion) will be doing something like that in the future, but for now it’s just the code and the steps. If you’re stuck on a specific issue, create a post in Dev and describe the issue in some detail.

I’ve added a sub-step to the topic custom fields plugin demonstrating how to preload fields if you’re using them in the topic list.

https://github.com/pavilionedu/discourse-topic-custom-fields/commit/bd34679589bcf27d63e922533856569495a77a76

You’ll need a category custom field to identify categories it should be displayed in. I’ve given category custom fields the same treatment in this plugin, complete with the same step-by-step instructions:

https://github.com/pavilionedu/discourse-category-custom-fields

Combining these two education plugins won’t take you all the way to your goal, but see if you can make it from there.

2 « J'aime »

That is fantastic, @angus. Thank you very much.

A video is always nice–I’m all for making things as straightforward as possible, but I don’t think it’s required to get the key value from these resources that @angus has put together. These resources give the code that you need to accomplish the specific goal the resource is about (having a working topic custom field or a category custom field). A video would probably just be @angus or someone else talking through how to implement the resource, but that is straightforward, and we can probably just lay it out here.

To be clear, these resources are not plugins that you just add to your site as a plug and play that customizes your forum. Rather, they efficiently give you the understanding you need to code your own custom fields in your plugin.


This is how I’ve used these resources:

You will need to add in the name and type of field that you want in config/settings. The code in these resources uses variables that are defined there. So you actually don’t need to do much customizing of the code to get it to work in your own plugin after that–the variables in plugin.rb and elsewhere refer to config/settings, and then should work.

After updating config/settings, you can just follow along with the code, adding it to your plugin:

  • Start with the code in plugin.rb, and add that to your own plugin’s plugin.rb in order to create the custom field

  • Then go to the initializer (at assets/javascripts/discourse/[custom-field-initiliazer]) to get the code that will initialize the custom field and allow you to save it to the server

  • Then create the form in the view layer that will be where the user (or your app, if the app adds the field automatically) can enter the value for the custom field, here (assets/discourse/connectors/[plugin-outlet-name]/[your special template].hbs.

  • @angus has set these up so you would add the forms for the custom fields in a plugin outlet that will be inserted in the discourse template. Settings for this form are here, (assets/javascripts/discourse/lib/[custom-field-name].js.es6, so you probably want to customize that too to make the form work.

@angus feel free to correct anything I’ve said here.

Once I got the feel for setting up the custom field by going through the steps above, I then started customizing things a bit further (for example, getting more creative with how the form works), but this was an extremely helpful starting point that saved me hours of work.

After going through it, I did have some questions (like I asked earlier), but getting responses in dev seems like the most helpful way to go about things from there.

3 « J'aime »

Great description! Yup, that’s how they’re intended to be used :+1:

1 « J'aime »

Edit: I originally posted my question regarding how to retrieve items based on a custom field here, but decided the question was sufficiently different to warrant its own post. So I posted separately here.

2 « J'aime »

I’m experiencing strange behavior with the composer following the topic custom fields example.

When I hit the “create topic” button (for example, on the category show page–but anywhere on the site), the composer fails to open, and I get this error:

Uncaught Error: Assertion Failed: The key provided to set must be a string or number, you passed undefined
    at assert (index.js:172)
    at set (index.js:2802)
    at Class.set (observable.js:176)
    at composer.js:769
    at Array.forEach (<anonymous>)
    at Class.open (composer.js:768)
    at composer.js:898
    at invokeCallback (rsvp.js:493)
    at rsvp.js:558
    at rsvp.js:19

I first started seeing this error when I tried to add a new ‘create topic’ button on a new page, but since then, even I remove that new button and even if I remove any related code, the error persists.

In some way, I think the following code–from the topic-custom-field-initializer–is causing the problem:

api.serializeOnCreate(fieldName);
api.serializeToDraft(fieldName);
api.serializeToTopic(fieldName, `topic.${fieldName}`);

When I remove this code, the create topic buttons work again (properly opening the composer). When I put the code back in, the composer error comes back.

I have had this code in my plugin previously without problem. But now it causes the composer error (even if I have removed any composer or create-topic button -related code in my plugin).

Of course, this code is important–it serializes the custom field. But it seems that it is conflicting with composer. Any ideas on how to fix?

I was able to figure out the cause–I was trying to add two separate custom fields to the topic custom fields initializer. For some reason that was causing some interference. There probably is a way to properly add two custom fields to that file, but my code, just repeating the same code for two separate custom fields, was causing problems. It worked again fine when I removed the second custom field from that file.

Avec ce code squelette, si je voulais ajouter plusieurs champs, chaque champ devrait-il être un plugin autonome ?

Je suis vraiment ravi d’avoir trouvé ce tutoriel et je me demande, combien, le cas échéant, de modifications devrais-je apporter à ces modèles pour qu’ils fonctionnent pour des champs d’utilisateur personnalisés ?

Non, il vous suffit d’ajouter du code supplémentaire pour le champ supplémentaire. Dans la plupart des cas, il suffit de dupliquer le code existant, par exemple :

add_preloaded_topic_list_custom_field(FIELD_NAME_1)
add_preloaded_topic_list_custom_field(FIELD_NAME_2)

Le premier endroit où chercher les champs personnalisés des utilisateurs est /admin/customize/user_fields, qui vous donne une interface utilisateur pour les ajouter. Si vous souhaitez un contrôle plus granulaire, le processus ressemble beaucoup à celui des sujets et des catégories, mais vous n’avez pas réellement besoin des éléments frontaux avec les champs utilisateurs.

En fait, nous (Pavilion) envisageons de créer un plugin de champs personnalisés (similaire à ACF pour WordPress) qui ressemblerait initialement un peu à l’interface d’administration des champs personnalisés du plugin Custom Wizard.

En fait, certaines personnes utilisent déjà le plugin Custom Wizard comme gestionnaire de champs personnalisés. Il répertorie tous les champs personnalisés de votre instance (de n’importe quelle source) et vous permet d’ajouter un champ de n’importe quel type à n’importe quel modèle qui les prend en charge.

Il n’ajoute pas de prise en charge frontale, par exemple comme celle montrée dans le plugin éducatif Topic Custom Field (et cela ne fonctionnerait pas dans le contexte du plugin Custom Wizard), c’est pourquoi nous envisageons de le séparer dans un plugin distinct.

3 « J'aime »

@angus, je pense que j’adorerais ça.

Surtout s’il ajoute le support côté frontend.

J’adorerais avoir un moyen facile pour les administrateurs d’ajouter des champs personnalisés à différentes classes, de permettre aux utilisateurs de les remplir (par exemple, pour les sujets, les messages, les profils d’utilisateurs), et d’avoir un moyen pour le frontend de les afficher.

La principale chose que je n’arrive pas à obtenir avec les champs d’utilisateurs personnalisés actuels est une variété de types de champs. Actuellement, il est limité à 4 je pense, et j’aimerais avoir les options disponibles avec le plugin Custom Wizards.

Idéalement, je veux construire un répertoire d’utilisateurs assez avancé, consultable/filtrable/triable, avec de nombreux champs personnalisés de nombreux types. J’expérimenterai avec Custom Wizards pour voir si cela fonctionnera pour l’instant et j’espère que vous investirez dans le plugin Custom Fields.

Merci !

@angus

Tout d’abord, merci beaucoup pour ce plugin.

Auriez-vous par hasard un exemple fonctionnel de ce plugin (champ personnalisé vers sujet) pour tenir compte de plusieurs champs personnalisés ? J’ai réussi à ajouter un champ personnalisé et à apporter quelques modifications sans problème.

J’ai essayé de dupliquer le code, de modifier et d’ajouter un plugin supplémentaire, etc.

Quelqu’un a-t-il un dépôt de code ou un exemple qu’il est prêt à partager avec moi ? Toute aide serait grandement appréciée.

1 « J'aime »

Salut @Joe_Stanton,

Je l’ai fait plusieurs fois, et la façon dont je l’ai fait a été de stocker les champs personnalisés dans un tableau avec un objet contenant un nom et un type pour le champ personnalisé.

Par exemple :

fields = [
  { name: 'isClassifiedListing', type: 'boolean' },
  { name: 'listingStatus', type: 'string' },
  { name: "listingDetails", type: 'json' }
]

Ensuite, je parcours les champs pour appliquer la logique mentionnée dans ce sujet. Vous pouvez voir un exemple de son utilisation dans un plugin sur lequel je travaille ici. Cependant, le code pertinent est ci-dessous.

Exemple

Côté serveur :

  # Enregistrement des champs personnalisés
  fields.each do |field|
    # Enregistre les champs
    register_topic_custom_field_type(field[:name], field[:type].to_sym)

    # Méthodes Getter
    add_to_class(:topic, field[:name].to_sym) do
      if !custom_fields[field[:name]].nil?
        custom_fields[field[:name]]
      else
        nil
      end
    end

    # Méthodes Setter
    add_to_class(:topic, "#{field[:name]}=") do |value|
      custom_fields[field[:name]] = value
    end

    # Mise à jour à la création du sujet
    on(:topic_created) do |topic, opts, user|
      topic.send("#{field[:name]}=".to_sym, opts[field[:name].to_sym])
      topic.save!
    end

    # Mise à jour lors de la modification du sujet
    PostRevisor.track_topic_field(field[:name].to_sym) do |tc, value|
      tc.record_change(field[:name], tc.topic.send(field[:name]), value)
      tc.topic.send("#{field[:name]}=".to_sym, value.present? ? value : nil)
    end

    # Sérialisation vers le sujet
    add_to_serializer(:topic_view, field[:name].to_sym) do
      object.topic.send(field[:name])
    end

    # Préchargement des champs
    add_preloaded_topic_list_custom_field(field[:name])

    # Sérialisation vers la liste des sujets
    add_to_serializer(:topic_list_item, field[:name].to_sym) do
      object.send(field[:name])
    end
  end

De même côté client pour sérialiser les champs :


 const CUSTOM_FIELDS = [
  { name: "isClassifiedListing", type: "boolean" },
  { name: "listingStatus", type: "string" },
  { name: "listingDetails", type: "json" },
];

  // Sérialisation des champs personnalisés :
  CUSTOM_FIELDS.forEach((field) => {
    api.serializeOnCreate(field.name);
    api.serializeToDraft(field.name);
    api.serializeToTopic(field.name, `topic.${field.name}`);
  });
2 « J'aime »

Merci @keegan !

Je pense avoir correctement configuré le fichier plugin.rb, la boucle semble correcte.

Cependant, j’ai des difficultés avec le fichier topic-custom-field-initializer.js. Voici mon code pour les deux fichiers. Des conseils pour le fichier initializer.js ? Je pense que je suis vraiment proche ici, lors de la création d’un nouveau sujet, j’obtiens 1/3 des champs, le champ listingDetails mais il me manque toujours isClassifiedListing et listingStatus.

enabled_site_setting :topic_custom_field_enabled
register_asset 'stylesheets/common.scss'

after_initialize do
  fields = [
  { name: 'isClassifiedListing', type: 'boolean' },
  { name: 'listingStatus', type: 'string' },
  { name: "listingDetails", type: 'json' }
]

 fields.each do |field|

  register_topic_custom_field_type(field[:name], field[:type].to_sym)

   add_to_class(:topic, field[:name].to_sym) do
      if !custom_fields[field[:name]].nil?
        custom_fields[field[:name]]
      else
        nil
      end
    end

   add_to_class(:topic, "#{field[:name]}=") do |value|
      custom_fields[field[:name]] = value
   end

   on(:topic_created) do |topic, opts, user|
      topic.send("#{field[:name]}=".to_sym, opts[field[:name].to_sym])
      topic.save!
   end

    PostRevisor.track_topic_field(field[:name].to_sym) do |tc, value|
      tc.record_change(field[:name], tc.topic.send(field[:name]), value)
      tc.topic.send("#{field[:name]}=".to_sym, value.present? ? value : nil)
    end

    add_to_serializer(:topic_view, field[:name].to_sym) do
      object.topic.send(field[:name])
    end

  add_preloaded_topic_list_custom_field(field[:name])

    # Serialize to the topic list
    add_to_serializer(:topic_list_item, field[:name].to_sym) do
      object.send(field[:name])
    end

end

end

Initializer.js

import { withPluginApi } from 'discourse/lib/plugin-api';
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from '@ember/object/computed';
import { isDefined, fieldInputTypes } from '../lib/topic-custom-field';

export default {
  name: "topic-custom-field-intializer",
  initialize(container) {


    const CUSTOM_FIELDS = [
      { name: "isClassifiedListing", type: "boolean" },
      { name: "listingStatus", type: "string" },
      { name: "listingDetails", type: "json" },
    ];

    CUSTOM_FIELDS.forEach((field) => {

    withPluginApi('0.11.2', api => {

      api.registerConnectorClass('composer-fields', 'composer-topic-custom-field-container', {
        setupComponent(attrs, component) {
          const model = attrs.model;

          if (!isDefined(model[field.name]) && model.topic && model.topic[field.name]) {
            model.set(field.name, model.topic[field.name]);
          }

          let props = {
            fieldName: field.name,
            fieldValue: model.get(field.name)
          }
          component.setProperties(Object.assign(props, fieldInputTypes(field.type)));
        },

        actions: {
          onChangeField(fieldValue) {
            this.set(`model.${field.name}`, fieldValue);
          }
        }
      });

      api.registerConnectorClass('edit-topic', 'edit-topic-custom-field-container', {
        setupComponent(attrs, component) {
          const model = attrs.model;

          let props = {
            fieldName: field.name,
            fieldValue: model.get(field.name)
          }
          component.setProperties(Object.assign(props, fieldInputTypes(field.type)));
        },

        actions: {
          onChangeField(fieldValue) {
            this.set(`buffered.${field.name}`, fieldValue);
          }
        }
      });

      api.serializeOnCreate(field.name);
      api.serializeToDraft(field.name);
      api.serializeToTopic(field.name, `topic.${field.name}`);

      api.registerConnectorClass('topic-title', 'topic-title-custom-field-container', {
        setupComponent(attrs, component) {
          const model = attrs.model;
          const controller = container.lookup('controller:topic');

          component.setProperties({
            fieldName: field.name,
            fieldValue: model.get(field.name),
            showField: !controller.get('editingTopic') && isDefined(model.get(field.name))
          });

          controller.addObserver('editingTopic', () => {
            if (this._state === 'destroying') return;
            component.set('showField', !controller.get('editingTopic') && isDefined(model.get(field.name)));
          });

          model.addObserver(field.name, () => {
            if (this._state === 'destroying') return;
            component.set('fieldValue', model.get(field.name));
          });
        }
      });

      api.modifyClass('component:topic-list-item', {
        customFieldName: field.name,
        customFieldValue: alias(`topic.${field.name}`),

        @discourseComputed('customFieldValue')
        showCustomField: (value) => (isDefined(value))
      });

    });


    });
  }
}

1 « J'aime »

Je ne l’ai pas testé, mais je pense que la raison pour laquelle vous ne voyez qu’un tiers des champs est qu’il boucle et enregistre une classe de connecteur non unique, remplaçant la précédente.

En général, pour le côté client, plutôt que de parcourir les champs personnalisés et de déclarer les méthodes d’API. Je suggère de définir des composants pour chaque champ séparément, ou du moins des actions séparées, car vous aurez probablement besoin d’une logique différente associée à chaque champ ?

La seule partie sur laquelle je bouclerais et déclarerais est la suivante :

  api.serializeOnCreate(field.name);
      api.serializeToDraft(field.name);
      api.serializeToTopic(field.name, `topic.${field.name}`);

Pour le reste des composants, il est probablement préférable de créer une logique distincte pour chaque cas.

3 « J'aime »

@keegan ça a fonctionné ! merci beaucoup pour toutes ces informations. Je n’aurais pas pu y arriver sans votre aide.

4 « J'aime »